Web开发路线:
Maven:
-
Maven依赖的Java环境:JDK11
安装Maven:
此处安装的maven版本是3.6.1
1.配置本地仓库
在maven目录下新建mvn_repo文件夹作为maven的本地仓库
打开maven目录下conf/setting.xml文件
更改下面配置语句
<localRepository>E:\Maven\apache-maven-3.6.1\mvn_repo</localRepository>
2.配置阿里云私服
<mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror>
3.配置环境变量
MAVEN_HOME 为maven的解压目录,并将其bin目录加入PATH环境变量
4.检查版本号
mvn -v
配置Maven环境(全局):
创建Maven项目:
导入maven项目:
Web开发:
HTTP--请求数据:
请求数据格式分为 请求行 请求头 请求体
请求方式:GET 请求参数在请求行中,没有请求体 GET请求大小是有限制的
请求方式:POST 请求参数在请求体中,POST请求大小是没有限制的
HTTP响应格式:
HTTP/1.1 200 OK
响应数据第一行(协议,状态码,描述)
响应头:key : value格式的数据
响应体:存放响应数据
-
响应状态码
常用响应码:
200 客户端请求成功
404 请求资源不存在
500 服务器发生错误
Web服务器:
对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作 , 提供网上信息浏览功能
Tomcat:
轻量级的web服务器,支持servlet , jsp 等少量javaEE规范
也被称为web容器,servlet容器
基本使用:
1.启动:
双击 bin\startup.bat
注意,前期是配置好JDK的环境变量JAVA_HOME
2.解决中文乱码:
修改conf/logging.properties
修改编码格式从UTF-8 改为GBK
java.util.logging.ConsoleHandler.encoding = GBK
3.配置Tomcat的端口号:
conf/server.xml文件
4.部署项目
将项目放置到webapps目录下,即部署完成
请求:
1.1 简单参数:
定义同名的形参即可接收参数
如果方法形参名称与请求参数名称不匹配,可以使用@RequestParam 完成映射
@RequestMapping("/simpleParam") public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age) { System.out.println(username + " " + age); return "ok"; }
注意事项:
-
@RequestParam中的required属性默认为true,代表该请求参数必须传递
-
将required属性设置为false,表示该参数是可选的
-
defaultValue:默认参数值,如果没有传该参数,就使用默认值
1.2 实体参数:
简单实体参数:
规则:请求参数名与形参对象属性名相同,即可通过POJO接收
//简单实体参数类型 @RequestMapping("/simplePojo") public String simplePojo(User user){ System.out.println(user); return "ok"; }
public class User { private String name; private Integer age; //省略getter和setter方法 }
复杂实体对象:
复杂实体对象的封装,需要遵守如下规则:
-
请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。
1.3 数组集合参数:
应用场景:
对复选框传递过来的多个参数进行封装
数组参数:
定义数组类型形参即可接收参数
//传递数组参数 @GetMapping("/arrayParam") public String arrayParam(String [] hobby){ System.out.println( Arrays.toString(hobby)); return "ok"; }
postman中模拟:
http://localhost:8080/arrayParam?hobby=game&hobby=java
集合参数:
@RequestParam 绑定参数关系
//集合参数 // @RequestParam绑定参数关系 @RequestMapping("/listParam") public String listParam(@RequestParam List<String> hobby){ System.out.println(hobby); return "ok"; }
http://localhost:8080/arrayParam?hobby=game&hobby=java
1.4 日期参数:
对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。
-
@DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。
-
后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。
1.5 JSON参数:
JSON参数:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用@RequestBody 标识
//传递JSON数据 @PostMapping("/jsonParam") public String jsonParam(@RequestBody User user) { System.out.println(user); return "ok"; }
控制台返回:
User{name='Tom', age=18}
1.6 路径参数:
通过请求URL直接传递参数 ,需要使用@PathVariable获取路径参数
//路径参数 @RequestMapping("/path/{id}") public String pathParam(@PathVariable Integer id){ System.out.println(id); return "ok"; }
传递多个路径参数:
//路径参数 @RequestMapping("/path/{id}/{name}") public String pathParam(@PathVariable Integer id,@PathVariable String name) { System.out.println(id+" "+name); return "ok"; }
响应:
响应数据:
controller方法中的return的结果,怎么就可以响应给浏览器呢?
答案:使用@ResponseBody注解
@ResponseBody:
类型 : 方法注解 ,类注解
位置: Controller方法上 / 类上
作用: 将方法返回值直接响应 , 如果返回值类型是 实体对象/集合 , 将会转换为JSON格式响应
说明: @RestController = @Controller + @ResponseBody
统一响应结果:
/** * 统一响应结果封装类 */ public class Result { private Integer code;//1 成功 , 0 失败 private String msg; //提示信息 private Object data; //数据 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 + '}'; } }
分层解耦:
IOC和DI:
控制反转:简称IOC。Service层及Dao层的实现类,交给IOC容器管理。IOC容器可以创建他们的对象
依赖注入:简称DI。为Controller及Service注入运行时,依赖的对象
Bean对象:IOC容器中创建的对象,称之为bean
IOC详解:
Bean的声明:
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解,(一般不用) | 不属于以下三类时,用此注解(一些工具类) |
@Controller | @Component的衍生注解 | Controller层 |
@Service | @Component的衍生注解 | Service层 |
@Repository | @Component的衍生注解 | Dao层(由于与mybatis整合,用的少) |
注意:
-
声明bean的时候,bean的名字默认为类名首字母小写。也可以通过value指定bean的名字
-
在springboot集成web开发中,声明控制器bean只能用@Controller
Bean组件扫描:
@SpringBootApplication 具有包扫描作用,默认扫描范围启动类所在包及其子包
DI详解:
Bean注入:
如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况
@Service public class DeptServiceImplA implements DeptService {} @Service public class DeptServiceImplB implements DeptService {} public class EmpController { @Autowired private EmpService empService; }
解决方案:
@Primary注解 , 在Service类上加上@Primary 注解
@Primary @Service public class DeptServiceImplA implements DeptService {} @Service public class DeptServiceImplB implements DeptService {}
@Resource注解
是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
@Autowired @Resource(name = "deptServiceImplA") private DeptService deptService;
Mybatis:
Mybatis 时一款持久层框架 , 用于简化JDBC的开发
@Mapper 注解:
@Mapper //在运行时,会自动生成该接口的实现类对象(代理对象),并且将该对象交给IOC容器管理 public interface UserMapper { //查询全部用户信息 @Select("select * from mybatis.user") public List<User> list(); }
配置SQL提示:
数据库连接池:
数据库连接池是个容器,负责分配,管理数据库连接
优势:资源复用,提升响应速度
标准接口 DataSource
功能:获取连接
lombok:
通过注解的方式自动生成构造器
Mybatis基础操作:
-
删除
-
插入
-
更新
-
查询
日志输出:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
查询操作:
数据响应案例:
可以看到这里的gender , job , deptId 都是number型的,解析成对应的数据会在前端完成
{ "code": 1, "msg": "success", "data": { "total": 2, "rows": [ { "id": 1, "username": "jinyong", "password": "123456", "name": "金庸", "gender": 1, "job": 2, "entrydate": "2015-01-01", "deptId": 2, }, ] } }
条件查询:
@Mapper public interface EmpMapper { @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); }
删除操作:
Mapper
public interface EmpMapper { @Delete("delete from emp where id=#{id}") void delete(Integer id); }
新增操作:
//新增数据 @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time)" + "VALUES (#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})") void insert(Emp emp);
新增(主键返回 ):
@Options(keyProperty = "id" , useGeneratedKeys = true) @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time)" + "VALUES (#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})") void insert(Emp emp);
更新操作:
//更新数据 @Update("update emp set username=#{username},name= #{name},gender=#{gender} where id = #{id}") void update(Emp emp);
数据封装:
实体类属性名 和 数据库表查询返回的字段名不一致 ,不能自动封装
解决方案:开启mybatis 的驼峰命名自动映射开关
mybatis: map-underscore-to-camel-case: true
XML映射文件:
内容:
<?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"> <!-- namespace里写EmpMapper的全限定名 --> <mapper namespace="com.example.mapper.EmpMapper"> </mapper>
MybatisX :
作用 : 快速定位XML对应的接口方法
Mybatis动态SQL:
<sql>:定义可重用的SQL片段
<include>:通过属性refid , 指定包含的SQL片段
SQL片段: 抽取重复的代码
<sql id="commonSelect"> select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp </sql>
然后通过<include>
标签在原来抽取的地方进行引用。操作如下:
<select id="list" resultType="com.itheima.pojo.Emp"> <include refid="commonSelect"/> <where> <if test="name != null"> name like concat('%',#{name},'%') </if> <if test="gender != null"> and gender = #{gender} </if> <if test="begin != null and end != null"> and entrydate between #{begin} and #{end} </if> </where> order by update_time desc </select>
SpringBoot案例:
前端页面:
开发接口一定要看接口文档
EditStarters插件
1.打开pom.xml文件
2.使用EditStarters快捷键 , 选择edit starters
alt + ins
3.搜索添加依赖
员工管理:
调试后端:
在控制层中写完接口方法后就可以先用postman来调试接口方法,
没必要把service层和mapper层都写完再进行调试
条件分页查询:
分页插件PageHeleper , 只能在Springboot 2.x 的版本使用
准备工作:
PageBean 对象类
@Data @NoArgsConstructor @AllArgsConstructor public class PageBean { private Long total; //总记录数 private List rows; //当前页数据列表 }
在xml文件中引入依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency>
代码实现:
EmpController
@Slf4j @RestController @RequestMapping("/emps") public class EmpController { @Autowired private EmpService empService; //条件分页查询 @GetMapping public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, String name, Short gender, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { //记录日志 log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize,name, gender, begin, end); //调用业务层分页查询功能 PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end); //响应 return Result.success(pageBean); } }
EmpService
public interface EmpService { /** * 条件分页查询 * @param page 页码 * @param pageSize 每页展示记录数 * @param name 姓名 * @param gender 性别 * @param begin 开始时间 * @param end 结束时间 * @return */ PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end); }
EmpServiceImpl
@Slf4j @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) { //设置分页参数 PageHelper.startPage(page, pageSize); //执行条件分页查询 List<Emp> empList = empMapper.list(name, gender, begin, end); //获取查询结果 Page<Emp> p = (Page<Emp>) empList; //封装PageBean PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); return pageBean; } }
EmpMapper
@Mapper public interface EmpMapper { //获取当前页的结果列表 public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end); }
EmpMapper.xml
<?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"> <mapper namespace="com.itheima.mapper.EmpMapper"> <!-- 条件分页查询 --> <select id="list" resultType="com.itheima.pojo.Emp"> select * from emp <where> <if test="name != null and name != ''"> name like concat('%',#{name},'%') </if> <if test="gender != null"> and gender = #{gender} </if> <if test="begin != null and end != null"> and entrydate between #{begin} and #{end} </if> </where> order by update_time desc </select> </mapper>
阿里云OSS:
阿里云服务:
提供了很多云服务,语音服务,短信服务,身份认证,翻译服务,对象存储服务等等
云存储服务
开通对象存储服务(OSS) --> 创建bucket ---> 获取AccessKey密钥 ---> 参照官方SDK编写入门程序 ---> 案例集成OSS
登录功能:
请求数据样例:
{ "username": "jinyong", "password": "123456" }
登录功能本质上和查询功能是一样的,在数据库中查找是否有这个数据,
如果有,也需要返回数据 , 而且需要用返回数据来判断是否存在这个用户
@PostMapping("/login") public Result login(@RequestBody Emp emp) { // 我自己写的时候我觉得 没有返回值也可以吧 ---->不可以 //没有返回值的逻辑是 在Mapper层将查询到的数据不返回 // 其实登录的逻辑和查询的逻辑是一样的,在数据库里查找有没有这个用户,如果有就返回 // 删除操作是你接收删除的id , 在数据库里直接删除 Emp emp1 = empService.login(emp); return emp1 != null ? Result.success() : Result.error(); }
登录校验:
会话跟踪方案
-
客户端会话跟踪技术:Cookie
-
服务端会话跟踪技术:Session
-
令牌技术
JWT令牌技术:
Base64:是一种基于64个可打印字符( A-Z a-z 0-9 + / )来表示二进制数据的编码方式
JWT组成:
第一部分:Header(头),记录令牌类型,签名算法等。
第二部分:Payload(载荷) ,携带一些自定义信息,默认信息等
第三部分:Signature(签名),防止Token被篡改,确保安全性。将header,patyload,并加入指定秘钥,通过指定签名算法计算而来
登录认账:
登录成功后,生成令牌
后续每个请求,都要携带令牌,系统在每次请求处理之前,先校验令牌,通过后,在处理。
JWT-生成:
-
引入JWT依赖
<!-- JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
-
Test类:
//生成JWT令牌 @Test public void testJWt() { Map<String, Object> cliams = new HashMap<>(); cliams.put("id", 1); cliams.put("name", "tom"); String jwtBuilder = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "hello jwt") //签名算法 .setClaims(cliams) //设置自定义内容 .setExpiration(new Date(System.currentTimeMillis() + 1000 * 3600)) //设置令牌有效期 .compact(); //生成字符串 System.out.println(jwtBuilder); //解析JWT令牌 //解析JWT令牌的方法 //解析JWT令牌的方法 @Test public void testParseJwt() { Claims claims = Jwts.parser() .setSigningKey("hello jwt") //指定签名秘钥 // 解析令牌 .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5NjgyMDAxMn0.sOXhIAaQ-YFKonaqozFjtxGpHS40mJn3QtOC88B41QY") .getBody(); System.out.println(claims);
注意:
-
JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
-
如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。
下发令牌:
环境依赖
<!-- JWT依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
JWT工具类
public class JwtUtils { private static String signKey = "itheima";//签名密钥 private static Long expire = 43200000L; //有效时间 /** * 生成JWT令牌 * @param claims JWT第二部分负载 payload 中存储的内容 * @return */ 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; } /** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分负载 payload 中存储的内容 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey)//指定签名密钥 .parseClaimsJws(jwt)//指定令牌Token .getBody(); return claims; } }
登录成功,生成JWT令牌并返回
@RestController @Slf4j public class LoginController { //依赖业务层对象 @Autowired private EmpService empService; @PostMapping("/login") public Result login(@RequestBody Emp emp) { //调用业务层:登录功能 Emp loginEmp = empService.login(emp); //判断:登录用户是否存在 if (loginEmp != null) { //自定义信息 Map<String, Object> claims = new HashMap<>(); claims.put("id", loginEmp.getId()); claims.put("username", loginEmp.getUsername()); claims.put("name", loginEmp.getName()); //使用JWT工具类,生成身份令牌 String token = JwtUtils.generateJwt(claims); return Result.success(token); } return Result.error("用户名或密码错误"); } }
校验令牌:
过滤器(Filter):
将资源的请求拦截下来,实现一些通用的操作
基本使用:
1.定义一个类实现Filter接口,并重写其所有方法
2.配置Filter:Filter类上加@WebFilter注解,配置拦截资源的路径。启动项上加@ServletComponentScan 开启Servlet组件支持
@WebFilter(urlPatterns = "/*") public class FilterImpl implements Filter { // init() 和 destored() 会默认实现,可以不重写 // 只实现doFilter() }
执行流程:
放行后访问完对应资源 , 还会回到Filter中
回到Filter中,执行放行后的逻辑
// 拦截请求时,调用该方法,可调用多次 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("拦截到了请求...放行前的逻辑"); //放行 filterChain.doFilter(servletRequest,servletResponse); System.out.println("拦截到了请求...放行后的逻辑"); }
过滤器链
一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
配置的多个Filter , 优先级是按照过滤器类名(字符串)的自然排序
登录校验:
代码实现
@Slf4j @WebFilter(urlPatterns = "/login") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; //1.请求url地址 String url = req.getRequestURI().toString(); log.info("请求的url:{}", url); //2.判断请求url中是否包含login.如果包含,则说明是登录操作,放行 if (url.contains("login")) { log.info("登录操作,放行...."); chain.doFilter(request, response); return; } //3.获取请求头中的令牌(token) String jwt = req.getHeader("token"); //4.判断令牌是否存在,不存在则返回登录失败 if (!StringUtils.hasLength(jwt)) { log.info("请求头token为空,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); // 手动转化 对象--json ----->阿里巴巴 fastJSON //把Result对象转化为JSON格式字符串 String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return; } //5.令牌存在,解析令牌。解析令牌失败,返回登录失败 try { JwtUtils.parseJWT(jwt); } catch (Exception e) { //jwt解析失败 e.printStackTrace(); log.info("解析令牌失败,返回未登录的信息"); Result error = Result.error("NOT_LOGIN"); // 手动转化 对象--json ----->阿里巴巴 fastJSON //把Result对象转化为JSON格式字符串 String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return; } //6.令牌解析成功,放行 log.info("令牌合法,放行"); chain.doFilter(request, response); } }
拦截器Interceptor:
Spring框架中提供的,用来动态拦截控制器方法的执行。
快速入门
-
定义拦截器,实现HandlerInterceptor接口,并重写其所有方法
-
注册拦截器
拦截路径:
在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,
调用excludePathPatterns("不拦截路径")
方法,指定哪些资源不需要拦截。
在拦截器中除了可以设置/**
拦截所有资源外,还有一些常见拦截路径设置:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/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,不能匹配/emps/1 |
登录校验:
代码实现:
定义拦截器
@Component @Slf4j public class LoginChenkInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.请求url地址 String url = request.getRequestURI(); log.info("请求的url地址是:{}", url); // 2.判断请求的url中是否包含login,如果包含就放行 if (url.contains("login")) { log.info("登录请求,放行"); return true; } // 3.url中不包含login,就去获取请求头中的令牌 String jwt = request.getHeader("token"); // 4.判断令牌是否存在,令牌不存在就返回,存在放行 if (!StringUtils.hasLength(jwt)) { log.info("请求头token为空,返回"); Result error = Result.error("NOT_NOLOGIN"); // 把对象转化为JSON返回 String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false; } // 5. 这一步表示令牌存在,解析令牌。解析成功,放行,解析失败,返回 // 令牌解析失败会出现异常,这里用try...catch来写 try { JwtUtils.parseJWT(jwt); } catch (Exception e) { e.printStackTrace(); Result error = Result.error("NOT_LOGIN"); //把对象转化为JSON返回 String notLogin = JSONObject.toJSONString(error); response.getWriter().write(notLogin); return false; } // 6.令牌解析成功,放行 log.info("令牌解析成功,放行"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("目标方法运行后执行"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("视图渲染完毕后运行,最后运行"); } }
注册配置拦截器:
@Configuration //配置类 public class WebConfig implements WebMvcConfigurer { @Autowired private LoginChenkInterceptor loginChenkInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginChenkInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); } }
异常处理:
全局异常处理器:
//全局异常处理器 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) //捕获所有异常 public Result ex(Exception ex) { ex.printStackTrace(); return Result.error("对不起,请联系管理员"); } }
事务管理:
事务是一组操作的集合。这组操作要么同时成功,要么同时失败。
案例:
解散部门
需求:当部门解散了不仅需要把部门信息删除了,还需要把该部门下的员工数据也删除了。
//开始事务管理 //删除部门的同时删除部门下的员工信息 @Transactional @Override public void delete(Integer id) { deptMapper.delete(id); //删除部门 int i = 1 / 0; //模拟抛出异常 empMapper.deleteByDeptId(id); //删除员工 }
说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
#spring事务管理日志 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
事务进阶:
事务属性---回滚
rollbackFor
默认情况下,只有出现RuntimeException才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
@Transactional(rollbackFor = Exception.class) @Override public void delete(Integer id) throws Exception { deptMapper.delete(id); if (true) { throw new Exception("出错啦"); //模拟抛出异常 } empMapper.deleteByDeptId(id); }
事务属性----传播行为
当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
@Transactional(rollbackFor = Exception.class ,propagation = Propagation.REQUIRED)
REQUIRED ---> 默认值,需要事务,有则加入,无则创建新事物
AOP基础:
什么是AOP?
面向切面编程 或者 面向特定方法编程。
场景:
案例部分功能运行缓慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时
1.获取方法运行开始时间
2.运行原始方法
3.获取方法运行结束时间
实现:
动态代理时面向切面编程的最主流的实现。SpringAOP旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程
AOP快速入门:
导入依赖:
<!-- AOP依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
编写AOP程序,针对于特定方法根据业务需要进行编程
AOP测试类:TimeAspect
@Slf4j @Component @Aspect //AOP类 public class TimeAspect { @Around("execution(* com.itheima.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() + "方法执行耗时:{}ms", end - begin); return result; } }
AOP核心概念:
连接点 JoinPoint , 可以被AOP控制的方法。
通知:Advice,指哪些重复的逻辑,也就是共性功能
切入点:PointCut , 被AOP控制的方法
AOP进阶:
通知类型:
@Around(重点) : 环绕通知,此注解标注的通知方法在目标方法前,后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@Around 环绕通知需要自己调用ProceedingJoinPoint.proceed( ) 来让原始方法执行,其它通知不需要考虑目标方法执行。
@Around 环绕通知方法的返回值,必须指定为Object ,来接收原始方法的返回值 。
通知顺序:
切入点表达式:
常用形式:1.execution(...) : 根据方法的签名来匹配
2.@annotation(...) : 根据注解匹配
AOP案例:
将增,删, 改相关接口的操作日志记录到数据库表中
日志信息包括:操作人,操作时间,执行方法的全类名,执行方法名,方法运行时参数,返回值,方法执行时长
思路分析:
1.对所有业务类中的增,删,改方法添加统一功能,使用AOP技术最为方便,
2.由于增,删,改方法名没有规律,可以自定义@Log 注解完成目标方法匹配
SpringBoot原理篇:
配置文件的优先级:
优先级排名(从高到低):
application.properties
application.yml
application.yaml
自动配置:
SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发。
自动配置原理
Web后端开发总结:
Maven高级:
分模块设计与开发:
创建maven模块,存放实体类
创建maven模块,存放相关工具类
注意:分模块开发需要先针对模块进行设计,在进行编码。不会先将工程开发完毕,然后进行拆分。
<!-- tlias-pojo依赖 --> <dependency> <groupId>com.itheima</groupId> <artifactId>tlias-pojo</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!-- tlias-utils依赖--> <dependency> <groupId>com.itheima</groupId> <artifactId>tlias-utils</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
继承与聚合:
子工程可以继承父工程中的依赖,简化依赖配置,统一管理依赖。
实现:
1,创建maven模块tlias-parent , 该工程为父工程,设置默认打包方式为pom( 默认jar包 )
2,在子工程中配置继承关系
3,在父工程中配置各个工程共有的依赖
若父子工程都配置了同一个依赖的不同版本,以子工程的为准。
版本锁定:
在maven中,可以在父工程的pom文件中通过 <dependencyManagement>
来统一管理依赖版本。
父工程:
<!--统一管理依赖版本--> <dependencyManagement> <dependencies> <!--JWT令牌--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies> </dependencyManagement>
子工程:
<dependencies> <!--JWT令牌--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency> </dependencies>
注意:
-
在父工程中所配置的
<dependencyManagement>
只能统一管理依赖版本,并不会将这个依赖直接引入进来。 这点和<dependencies>
是不同的。 -
子工程要使用这个依赖,还是需要引入的,只是此时就无需指定
<version>
版本号了,父工程统一管理。变更依赖版本,只需在父工程中统一变更。
属性配置
我们也可以通过自定义属性及属性引用的形式,在父工程中将依赖的版本号进行集中管理维护。
1). 自定义属性
<properties> <lombok.version>1.18.24</lombok.version> </properties>
2). 引用属性
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency>
接下来,我们就可以在父工程中,将所有的版本号,都集中管理维护起来。
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <lombok.version>1.18.24</lombok.version> <jjwt.version>0.9.1</jjwt.version> <aliyun.oss.version>3.15.1</aliyun.oss.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> </dependencies> <!--统一管理依赖版本--> <dependencyManagement> <dependencies> <!--JWT令牌--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency> <!--阿里云OSS--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>${aliyun.oss.version}</version> </dependency> </dependencies> </dependencyManagement>
版本集中管理之后,我们要想修改依赖的版本,就只需要在父工程中自定义属性的位置,修改对应的属性值即可。
聚合:
继承关系下,打包相互依赖的子工程,需要先把子工程依赖的其它模块安装到maven的本地仓库,然后才能打包
maven聚合可以实现一键构建,打包等功能
实现
在maven中,我们可以在聚合工程中通过 <moudules>
设置当前聚合工程所包含的子模块的名称。我们可以在 tlias-parent中,添加如下配置,来指定当前聚合工程,需要聚合的模块:
<!--聚合其他模块--> <modules> <module>../tlias-pojo</module> <module>../tlias-utils</module> <module>../tlias-web-management</module> </modules>
那此时,我们要进行编译、打包、安装操作,就无需在每一个模块上操作了。只需要在聚合工程上,统一进行操作就可以了。
继承与聚合对比:
-
作用
-
聚合用于快速构建项目
-
继承用于简化依赖配置、统一管理依赖
-
-
相同点:
-
聚合与继承的pom.xml文件打包方式均为pom,通常将两种关系制作到同一个pom文件中
-
聚合与继承均属于设计型模块,并无实际的模块内容
-
-
不同点:
-
聚合是在聚合工程中配置关系,聚合可以感知到参与聚合的模块有哪些
-
继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己
-