黑马JavaWeb学习笔记


Web案例相关技术

三层架构


在三层架构当中,前端发起请求首先会到达Controller(不进行逻辑处理),然后Controller
会直接调用Service 进行逻辑处理, Service再调用Dao完成数据访问操作。

如果我们在执行具体的业务处理之前,需要去做一些通用的业务处理,比如:我们要进行统一的登录校
验,我们要进行统一的字符编码等这些操作时,我们就可以借助于Javaweb当中三大组件之一的过滤器
Filter或者是Spring当中提供的拦截器Interceptor来实现。

具体实现时,在controller层用@Autowirte注入对应Service接口,调用Service中的方法,

Service接口中声明方法(包括返回值,参数),

具体实现在ServiceImpl中代码实现,
ServiceImpl中注入Mapper层接口,调用Mapper层接口中的方法操作数据库,

Mapper层中用注解(@select,@Delete,@insert等注解)

或者映射xml配置文件执行数据库操作语句。

<mapper namespace="com.itheima.mapper.EmpMapper">
    <update id="update">
       update emp
        <set>
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="password != null and password != ''">
                password = #{password},
            </if>
            <if test="name != null and name != ''">
                name = #{name},
            </if>
            <if test="gender != null">
                gender = #{gender},
            </if>
            <if test="image != null and image != ''">
                image = #{image},
            </if>
            <if test="job != null">
                job = #{job},
            </if>
            <if test="entrydate != null">
                entrydate = #{entrydate},
            </if>
            <if test="deptId != null">
                dept_id = #{deptId},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime}
            </if>
        </set>
       where id=#{id};
    </update>

开发规范

1.开发规范-REST
当前最为主流的前后端分离模式进行开发。
在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发。
REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。

通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
GET : 查询
POST :新增
PUT :修改
DELETE :删除

使用rest风格开发时,注解要使用对应的GetMapping,PostMapping,DeleteMapping注解等
注:REST是风格,是约定方式,约定不是规定,可以打破
描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。
如:users、emps、books…
2.开发规范-统一响应结果
前后端工程在进行交互时,使用统一响应结果 Result。

package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
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);
 }
}

说明:只要按照接口文档开发功能接口,就能保证前后端程序交互
后端:严格遵守接口文档进行功能接口开发
前端:严格遵守接口文档访问功能接口
开发流程

注解相关

在controller中接收请求路径中的路径参数
@PathVariable

@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id) {
//日志记录
log.info("根据id删除部门");
//调用service层功能
deptService.delete(id);
//响应
return Result.success();
 }
//省略...
}

在controller中接收json格式的请求参数
@RequestBody //把前端传递的json数据填充到实体类中

@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){
//记录日志
log.info("新增部门:{}",dept);
//调用service层添加功能
deptService.add(dept);
//响应
return Result.success();
 }
//省略...
}

@requestParam可以将前端的请求参数赋值给方法中的形参,前后端名称一致(不一致设置value值)
跟随在请求路径后的参数字符串。 例:/emps?page=1&pageSize=10

 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

    )

@ConfigurationProperties
在Spring中给我们提供了一种简化方式,可以直接将配置文件中配置项的值自动的注入对象的属性中。

  1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

2.需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致
比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当
中的属性还需要提供 getter / setter方法
3.需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象
4.在实体类上添加 @ConfigurationProperties 注解,并通过prefix属性来指定配置参数项的前缀

实体类:AliOSSProperties

import lombok.Data;
import
org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/*阿里云OSS相关配置*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
//区域
private String endpoint;
//身份ID
private String accessKeyId ;
//身份密钥
private String accessKeySecret ;
//存储空间
private String bucketName;
}

AliOSSUtils工具类:

public class AliOSSUtils {
//注入配置参数实体类对象
@Autowired
private AliOSSProperties aliOSSProperties;

之前的工具类

@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;
 
 //省略其他代码...
 } 

@Value注解只能一个一个的进行外部属性的注入。
@ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。

如果要注入的属性非常的多,并且还想做到复用,就可以定义这么一个bean对象。通过
configuration properties 批量的将外部的属性配置直接注入到 bin 对象的属性当中。在其他
的类当中,我要想获取到注入进来的属性,我直接注入 bin 对象,然后调用 get 方法,就可以获取
到对应的属性值了

请求路径简化

在Spring当中为了简化请求路径,可以把公共的请求路径,直接抽取到类上,在类上加一个注解**@RequestMapping**,并指定请求路径"/depts"(例子)。代码参照如下:

一个完整的请求路径,应该是类上@RequestMapping的value属性 + 方法上的
@RequestMapping(或者是post/get mapping)的value属性

个别功能实现

分页查询插件
分页查询功能编写起来比较繁琐原始方式的分页查询,存在着"步骤固定"、"代码频繁"的问题
解决方案:可以使用一些现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper
分页插件帮我们完成了以下操作:

  1. 先获取到要执行的SQL语句:select * from emp
  2. 把SQL语句中的字段列表,变为:count(*)
  3. 执行SQL语句:select count(*) from emp //获取到总记录数
  4. 再对要执行的SQL语句:select * from emp 进行改造,在末尾添加 limit ? ,
    ?
  5. 执行改造后的SQL语句:select * from emp limit ? , ?
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>

Mapper层:定义一个查询所有的方法

@Mapper
public interface EmpMapper {
//获取当前页的结果列表
@Select("select * from emp")
public List<Emp> page(Integer start, Integer pageSize);
}

Service接口中声明方法即可。
ServiceImpl中使用PageHelper的方法,并获取分页结果

@Override
public PageBean page(Integer page, Integer pageSize) {
// 设置分页参数
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;
}

分页条件查询
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接口中也添加对应参数

PageBean page(Integer page, Integer pageSize, String name, Short
gender, LocalDate begin, LocalDate end);
}

EmpServiceImpl中同样添加参数,执行修改的list方法,并获取

@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.xml (查询语句较为复杂,所以写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>

Mybatis中的动态SQL:foreach

<select id="delete">
 delete from emp where id in
<foreach collection="ids" item="id" open="(" close=")"
separator=",">
 #{id}
</foreach>
</select>

使用这个标签可以遍历请求中传递的多个id实现批量操作
MultipartFile
使用这个API就可以来接收到上传的文件

public Result upload(String username,
Integer age,
@RequestParam("image") MultipartFile
file)

MultipartFile 常见方法:
String getOriginalFilename(); //获取原始文件名
void transferTo(File dest); //将接收的文件转存到磁盘文件中
long getSize(); //获取文件的大小,单位:字节
byte[] getBytes(); //获取文件内容的字节数组
InputStream getInputStream(); //获取接收到的文件内容的输入流

@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile
image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+originalFilename));
return Result.success();
 }
}

阿里云OSS

云服务指的就是通过互联网对外提供的各种各样的服务,比如像:语音服务、短信服务、邮件服
务、视频直播服务、文字识别服务、对象存储服务等等。
别人帮我们实现好了功能,我们只要调用即可。
引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
@Component
public class AliOSSUtils {
private String endpoint = "https://oss-cnshanghai.aliyuncs.com";
private String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
private String accessKeySecret =
"C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
private String bucketName = "web-framework01";
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws
IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 避免文件覆盖
String originalFilename =
multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() +
originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint,
accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName +
"." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
 }
}

注:代码的前四项属性需要去自己的阿里云账号的oss对象存储开通服务,并查找,填写自己的。
修改UploadController代码:

import com.itheima.pojo.Result;
import com.itheima.utils.AliOSSUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@Slf4j
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public Result upload(MultipartFile image) throws IOException {
//调用阿里云OSS工具类,将上传上来的文件存入阿里云
String url = aliOSSUtils.upload(image);
//将图片上传完成后的url返回,用于浏览器回显展示
return Result.success(url);
 }
}

yml配置文件
yml 格式的配置文件,后缀名有两种:
yml (推荐)
yaml

可以使用插件

将文件格式转为yml/yaml

登录认证相关技术

而统一拦截技术现实方案也有两种:

  1. Servlet规范中的Filter过滤器
  2. Spring提供的interceptor拦截器
    会话技术
    在web开发当中,会话指的就是浏览器与服务器之间的一次连接,我们就称为一次会话。
    在用户打开浏览器第一次访问服务器的时候,这个会话就建立了,直到有任何一方断开连
    接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。
    第1次:访问的是登录的接口,完成登录操作
    第2次:访问的是部门管理接口,查询所有部门数据
    第3次:访问的是员工管理接口,查询员工数据
    只要浏览器和服务器都没有关闭,以上3次请求都属于一次会话当中完成的。
    由于HTTP是无状态协议,在后面请求中怎么拿到前一次请求生成的数据呢?此时就需要在一次会话的多次请求之间进行数据共享
    会话跟踪技术有三种:
  3. Cookie(客户端会话跟踪技术)
    数据存储在客户端浏览器当中
  4. Session(服务端会话跟踪技术)
    数据存储在储在服务端
  5. 令牌技术

cookie与session

cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP
协议官方给我们提供了一个响应头和请求头:
响应头 Set-Cookie :设置Cookie数据的
请求头 Cookie:携带Cookie数据
代码样例:

@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima"));
//设置Cookie/响应Cookie
return Result.success();
 }
 
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username:
"+cookie.getValue()); //输出name为login_username的cookie
 }
 }
return Result.success();
 }
}


优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携
带,都是浏览器自动进行的,是无需我们手动操作的)
缺点:
移动端APP(Android、IOS)中无法使用Cookie
不安全,用户可以自己禁用Cookie
Cookie不能跨域
session
Session,它是服务器端会话跟踪技术,所以它是存储在服务器端的。而
Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。

@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数return Result.success();
 }
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从
session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
 }
}

优点:Session是存储在服务端的,安全
缺点:服务器集群环境下无法直接使用Session
移动端APP(Android、IOS)中无法使用Cookie
用户可以自己禁用Cookie
Cookie不能跨域
服务器集群环境为何无法使用Session

我们现在所开发的项目,一般都不会只部署在一台服务器上,因为一台服务器
会存在一个很大的问题,就是单点故障。所谓单点故障,指的就是一旦这台服务器挂了,整
个应用都没法访问了

JWT令牌

JWT全称:JSON Web Token
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
第一部分:Header(头), 记录令牌类型、签名算法等。 例如{“alg”:“HS256”,“type”:“JWT”}
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:
{“id”:“1”,“username”:“Tom”}
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,
所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡
改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。
登录认证场景中JWT的作用

  1. 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成
    一个jwt令牌,将生成的 jwt令牌返回给前端。
  2. 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服
    务端。
  3. 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。
    JWT生成和校验
  4. 引入依赖
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

在引入完JWT来赖后,就可以调用工具包中提供的API来完成JWT令牌的生成和校验
工具类:Jwts
生成JWT代码实现:

@Test
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","Tom");
String jwt = Jwts.builder()
 .setClaims(claims) //自定义内容(载荷) 
 .signWith(SignatureAlgorithm.HS256, "itheima") //签名算法(itheima是签名密钥)
 .setExpiration(new Date(System.currentTimeMillis() +
24*3600*1000)) //有效期
 .compact();
System.out.println(jwt);
}

解析JWT令牌代码实现

@Test
public void parseJwt(){
Claims claims = Jwts.parser()
 .setSigningKey("itheima")//指定签名密钥(必须保证和生成令牌时使用
相同的签名密钥)
 
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMw
fQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
 .getBody();
System.out.println(claims);
}

令牌解析后,我们可以看到id和过期时间,如果在解析的过程当中没有报错,就说明解析成功了。
项目中实现JWT令牌

  1. 引入JWT工具类,在项目工程下创建com.itheima.utils包,并把提供JWT工具类复制到该包下
  2. 登录完成后,调用工具类生成JWT令牌并返回
    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("用户名或密码错误");
 }
}

通过以上操作,我们做到了在前端登陆成功时返回一个JWT令牌,下面学习过滤器和拦截器以实现JWT令牌的校验

过滤器

快速入门Filter

Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。
第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加@ServletComponentScan 开启Servlet组件支持。

@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 销毁方法执行了");
 }
}

在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解
@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。

@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class,
args);
 }
}
过滤器Filter详解
过滤器的执行流程


过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是
调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属
于放行之前的逻辑。
在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的
逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。

@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override //初始化方法, 只调用一次
public void init(FilterConfig filterConfig) throws
ServletException {
System.out.println("init 初始化方法执行了");
 }
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain filterChain) throws
IOException, ServletException {
System.out.println("DemoFilter 放行前逻辑.....");
//放行请求
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("DemoFilter 放行后逻辑.....");
 }
 @Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
 }
}
过滤器的拦截路径配置

@WebFilter(urlPatterns = "/login") //拦截/login具体路径
@WebFilter(urlPatterns = "/depts/*")//拦截所有以/depts开头,后面是什么无所谓
过滤器链

过滤器链指的是在一个web应用程序当中,可
以配置多个过滤器,多个过滤器就形成了一个过滤器链。


链的执行顺序
执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高。

案例实现


代码实现

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain) throws
IOException, ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子
类中特有方法)
HttpServletRequest request = (HttpServletRequest)
servletRequest;
HttpServletResponse response = (HttpServletResponse)
servletResponse;
//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:
http://localhost:8080/login
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain) throws
IOException, ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子
类中特有方法)
HttpServletRequest request = (HttpServletRequest)
servletRequest;
HttpServletResponse response = (HttpServletResponse)
servletResponse;
//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:
http://localhost:8080/login
}
//6.放行
chain.doFilter(request, response);
 }
}

以上就实现了退出登陆后若直接访问内部页面则会跳转到登录页面。

拦截器Interceptor

是一种动态拦截方法调用的机制,类似于过滤器。
拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。

  1. 定义拦截器:实现HandlerInterceptor接口,并重写其所有方法
//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //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 .... ");
 }
}

注:
preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
postHandle方法:目标资源方法执行后执行
afterCompletion方法:视图渲染完毕后执行,最后执行
2. 注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**
");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
 }
}
拦截器Interceptor详解

拦截路径
我们要指定拦截器的拦截路径,通过 addPathPatterns(“要拦截路径”) 方法,就可以指定要拦截哪些资源。
入门程序中我们配置的是 /** ,表示拦截所有资源,而在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用 excludePathPatterns(“不拦截路径”) 方法,指定哪些资源不需要拦截。

执行流程

当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在SpringWeb环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住执行preHandle() 方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。
controller当中的方法执行完毕之后,再回过来执行 postHandle() 这个方法以及
afterCompletion() 方法
,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。

异常处理

全局异常处理器
定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解**@RestControllerAdvice**,加上这个注解就代表我们定义了一个全局异常处理器。

在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解
@ExceptionHandler。通过**@ExceptionHandler注解**当中的value属性来指定我们要捕获的是哪一类型的异常。

@RestControllerAdvice
public class GlobalExceptionHandler {
//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息
//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
 }
}

@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端

SpringAOP&事务

事务

介绍

在数据库阶段我们已学习过事务了,我们讲到:事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败
事务的操作主要有三步:

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
    在spring中提供了Transactional注解来控制事务
    @Transactional注解书写位置:
    方法:
    当前方法交给spring进行事务管理
    类:
    当前类中所有的方法都交由spring进行事务管理
    接口:
    接口下所有的实现类当中所有的方法都交给spring 进行事务管理
事务进阶
  1. 异常回滚的属性:rollbackFor
    默认情况下,只有出RuntimeException(运行时异常)才会回滚事务。
    想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
@Transactional(rollbackFor=Exception.class)

在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。
2. 事务传播行为:propagation


对于这些事务传播行为,我们只需要关注以下两个就可以了:

  1. REQUIRED(默认值)一般用的最多
  2. REQUIRES_NEW
    REQUIRED :大部分情况下都是用该传播行为即可。
    REQUIRES_NEW :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订
    单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

AOP

基础

AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。

AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
面向指定的一个或多个方法进行编程,我们就称之为 面向切面编程。
AOP的优势:

  1. 减少重复代码
  2. 提高开发效率
  3. 维护方便
    所需依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOP程序:TimeAspect

@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.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;
 }
}

AOP相关概念
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
目标对象:Target,通知所应用的对象

进阶

通知类型
在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执

@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
抽取切入点表达式
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。

@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){
 }
//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint)
throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
 }
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info("after ...");
 }
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
 }
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
 }
}

通知顺序
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
如果我们想控制通知的执行顺序有两种方式:

  1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)
  2. 使用Spring提供的@Order注解
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小
越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
 }
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
 }
}
@Slf4j
@Component
@Aspect
@Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小
越后执行)
public class MyAspect3 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
 }
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
 }
}
@Slf4j
@Component
@Aspect
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小
越后执行)
public class MyAspect4 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect4 -> before ...");
 }
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect4 -> after ...");
 }
}

通知的执行顺序大家主要知道两点即可:
1. 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
2. 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序

切入点表达式
execution

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
通配符描述切入点:
“*”:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,
也可以通配包、类、方法名的一部分
“…” :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
总结

  1. 方法的访问修饰符可以省略
  2. 返回值可以使用 * 号代替(任意返回值类型)
  3. 包名可以使用 * 号代替,代表任意包(一层包使用一个 * )
  4. 使用 … 配置包名,标识此包以及此包下的所有子包
  5. 类名可以使用 * 号代替,标识任意类
  6. 方法名可以使用 * 号代替,表示任意方法
  7. 可以使用 * 配置参数,一个任意类型的参数
  8. 可以使用 … 配置参数,任意个任意类型的参数
@annotation

那么如果我们要匹配多个无规则的方法,用这种方法更方便
实现步骤:

  1. 编写自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
  1. 在业务类要做为连接点的方法上添加自定义注解
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
 }
@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
 }
@Override
public void save(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.save(dept);
 }
@Override
public Dept getById(Integer id) {
return deptMapper.getById(id);
 }
@Override
public void update(Dept dept) {
dept.setUpdateTime(LocalDateTime.now());
deptMapper.update(dept);
 }
}
  1. 切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.itheima.anno.MyLog)")
public void before(){
log.info("MyAspect6 -> before ...");
}
//后置通知
@After("@annotation(com.itheima.anno.MyLog)")
public void after(){
log.info("MyAspect6 -> after ...");
 }
}

总结

execution切入点表达式
根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过
execution切入点表达式描述比较繁琐
annotation 切入点表达式
基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

Springboot原理

配置优先级

application.properties
application.yml
application.yaml
在SpringBoot项目当中,我们要想配置一个属性,可以通过这三种方式当中的任意一种来配置都可以,默认的优先级:properties>yml>yaml
注意事项:虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置。(yml是主流)
除了以上三种配置方式,还有java系统属性配置和命令行参数,此二者优先级都高于前三者,且命令行参数优先级>Java系统属性配置

另外,项目打包上线后也可以通过命令行参数配置属性。
代码:
(后方的10010是命令行参数配置,前面的9000是java属性配置)

java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010



总结
在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。通过以上的测试,我们也得出了优先级(从低到高):
application.yaml(忽略)
application.yml
application.properties
java系统属性(-Dxxx=xxx)
命令行参数(–xxx=xxx)

bean的管理

如何从IOC容器中手动的获取到bean对象

在Spring容器中提供了一些方法,可以主动从IOC容器中获取到bean对象
根据name获取bean

Object getBean(String name)

根据类型获取bean

<T> T getBean(Class<T> requiredType)

根据name获取bean(带类型转换)

<T> T getBean(String name, Class<T> requiredType)

代码

@SpringBootTest
class SpringbootWebConfig2ApplicationTests {
@Autowired
private ApplicationContext applicationContext; //IOC容器对象
//获取bean对象
@Test
public void testGetBean(){
//根据bean的名称获取
DeptController bean1 = (DeptController)
applicationContext.getBean("deptController");
System.out.println(bean1);
//根据bean的类型获取
DeptController bean2 =
applicationContext.getBean(DeptController.class);
System.out.println(bean2);
//根据bean的名称 及 类型获取
DeptController bean3 =
applicationContext.getBean("deptController", DeptController.class);
System.out.println(bean3);
 }
}

运行结果:

输出的地址一样,说明IOC容器中的bean对象是单例(只有一个)
一般来说,Spring项目启动时,会把其中的bean都创建好,但是被@lazy修饰的类或方法会延迟加载,需要用到时bean才加载好。

bean的作用域配置

默认bean对象是单例模式(只有一个实例对象)
在Spring中支持五种作用域,后三种在web环境才生效:

可以借助Spring中的@Scope注解来进行配置作用域

IOC容器中的bean默认使用的作用域:singleton (单例)
默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到
第一次使用时)
prototype的bean,每一次使用该bean的时候都会创建一个新的实例
实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性

管理第三方的bean对象

如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到@Bean注解。
在配置类中定义@Bean标识的方法
如果需要定义第三方Bean时, 通常会单独定义一个配置类

@Configuration //配置类 (在配置类当中对第三方bean进行集中的配置管理)
public class CommonConfig {
//声明第三方bean
@Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
//通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认
是方法名
public SAXReader reader(DeptService deptService){
System.out.println(deptService);
return new SAXReader();
 }
}

注意:如果第三方bean需要依赖其它bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。
总结:
如果是在项目当中我们自己定义的类,想将这些类交给IOC容器管理,我们直接使用@Component
以及它的衍生注解来声明就可以。
如果这个类它不是我们自己定义的,而是引入的第三方依赖当中提供的类,而且我们还想将这个类
交给IOC容器管理。此时我们就需要在配置类中定义一个方法,在方法上加上一个@Bean注解,通
过这种方式来声明第三方的bean对象。

Springboot底层原理

通过SpringBoot所提供的起步依赖,就可以大大的简化pom文件当中依赖的配置,从而解决了Spring框架当中依赖配置繁琐的问题。
通过自动配置的功能就可以大大的简化框架在使用时bean的声明以及bean的配置。我们只需要引入程序开发时所需要的起步依赖,项目开发时所用到常见的配置都已经有了,我们直接使用就可以了

起步依赖

如果我们使用了SpringBoot,就不需要像上面这么繁琐的引入依赖了。我们只需要引入一个依赖就可以了,那就是web开发的起步依赖:springboot-starter-web。

比如:springboot-starter-web,这是web开发的起步依赖,在web开发的起步依赖当
中,就集成了web开发中常见的依赖:json、web、webmvc、tomcat等。我们只需要引入
这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。

原理:Maven的依赖传递。

自动配置

SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。

第三方依赖引入

@Component注解来声明bean对象时,还需
要保证@Component注解能被Spring的组件扫描到。
SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启
动类所在的当前包以及子包。
解决第三方依赖所在包不是启动类所在包
方案一
@ComponentScan组件扫描

@ComponentScan({"com.itheima","com.example"}) //指定要扫描的包

结论:SpringBoot中并没有采用以上这种方案。
方案二
@Import导入
导入形式主要有以下几种:
1.导入普通类

@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中
  1. 导入配置类
@Configuration
public class HeaderConfig {
@Bean
public HeaderParser headerParser(){
return new HeaderParser();
 }
@Bean
public HeaderGenerator headerGenerator(){
return new HeaderGenerator();
 }
}

启动类:

@Import(HeaderConfig.class) //导入配置类
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class,
args);
 }
}
  1. 导入ImportSelector接口实现类

ImportSelector接口实现类

public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata
importingClassMetadata) {
//返回值字符串数组(数组中封装了全限定名称的类)
return new String[]{"com.example.HeaderConfig"};
 }
}

启动类

@Import(MyImportSelector.class) //导入ImportSelector接口实现类
@SpringBootApplication
public class SpringbootWebConfig2Application {
public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class,
args);
 }
}

思考:如果基于以上方式完成自动配置,当要引入一个第三方依赖时,是不是还要知道第三方依赖中有哪些配置类和哪些Bean对象?
所以:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。

  1. 使用第三方依赖提供的 @EnableXxxxx注解
    第三方依赖中提供的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig {
}

在使用时只需在启动类上加上@EnableXxxxx注解即可

以上四种方式都可以完成导入操作,但是第4种方式会更方便更优雅,而这种方式也是SpringBoot当中所采用的方式。

源码跟踪

源码跟踪技巧:
在跟踪框架源码的时候,一定要抓住关键点,找到核心流程。一定不要从头到尾一行代码去看,一个方法的去研究,一定要找到关键流程,抓住关键点,先在宏观上对整个流程或者整个原理有一个认识,有精力再去研究其中的细节。

在@SpringBootApplication注解中包含了:
元注解(不再解释)
@SpringBootConfiguration
@SpringBootConfiguration注解上使用了@Configuration,表明SpringBoot启动类就是
一个配置类。

@ComponentScan
@ComponentScan注解是用来进行组件扫描的,扫描启动类所在的包及其子包下所有被
@Component及其衍生注解声明的类。
SpringBoot启动类,之所以具备扫描包功能(SpringBoot中默认扫描的是启动类所在的当前包及其子包),就是因为包含了@ComponentScan注解。
@EnableAutoConfiguration(自动配置核心注解)
封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类)

当SpringBoot程序启动时,就会加载配置文件当中所定义的配置类,并将这些配置类信息(类的全限定
名)封装到String类型的数组中,最终通过@Import注解将这些配置类全部加载到Spring的IOC容器中,交给IOC容器管理。

@conditional
在声明bean对象时,上面有加一个以@Conditional开头的注解,这种注解的
作用就是按照条件进行装配,只有满足条件之后,才会将bean注册到Spring的IOC容器中(下面会详细来讲解)
作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。
位置:方法、类
@Conditional本身是一个父注解,派生出大量的子注解:
@ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。
以这个注解为例,其他的使用方法大同小异

@Configuration
public class HeaderConfig {
@Bean
@ConditionalOnClass(name="io.jsonwebtoken.Jwts")//环境中存在指定的
这个类,才会将该bean加入IOC容器
public HeaderParser headerParser(){
return new HeaderParser();
 }
//省略其他代码...
}

@ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册
bean到IOC容器。

@ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。

总结
自动配置的核心就在@SpringBootApplication注解上,SpringBootApplication这个注解
底层包含了3个注解,分别是:
@SpringBootConfiguration
@ComponentScan
@EnableAutoConfiguration
@EnableAutoConfiguration这个注解才是自动配置的核心。
封装了一个@Import注解,Import注解里面指定了一个ImportSelector接口的实现类。
在这个实现类中,重写了ImportSelector接口中的selectImports()方法
而selectImports()方法中会去读取两份配置文件,并将配置文件中定义的配置类做为
selectImports()方法的返回值返回
,返回值代表的就是需要将哪些类交给Spring的IOC
容器进行管理。
那么所有自动配置类的中声明的bean都会加载到Spring的IOC容器中吗? 其实并不会,因
为这些配置类中在声明bean时,通常都会添加**@Conditional开头的注解,这个注解就是进
行条件装配**。而Spring会根据Conditional注解有选择性的进行bean的创建。
@Enable 开头的注解底层,它就封装了一个注解 import 注解,它里面指定了一个类,是ImportSelector 接口的实现类。在实现类当中,我们需要去实现 ImportSelector
接口当中的一个方法 selectImports 这个方法。这个方法的返回值代表的就是我需要将
哪些类交给 spring 的 IOC容器进行管理

此时它会去读取两份配置文件,一份儿是 spring.factories,另外一份儿是
autoConfiguration.imports。而在 autoConfiguration.imports 这份文件
当中,它就会去配置大量的自动配置的类

而前面我们也提到过这些所有的自动配置类当中,所有的 bean都会加载到 spring 的
IOC 容器当中吗?其实并不会,因为这些配置类当中,在声明 bean 的时候,通常会加上
这么一类@Conditional 开头的注解。这个注解就是进行条件装配。所以SpringBoot非
常的智能,它会根据 @Conditional 注解来进行条件装配。只有条件成立,它才会声明这
个bean,才会将这个 bean 交给 IOC 容器管理。

自定义starter

**在实际的项目开发当中,我们可能
会用到很多第三方的技术,并不是所有的第三方的技术官方都给我们提供了与SpringBoot整合的starter起步依赖,但是这些技术又非常的通用,在很多项目组当中都在使用。
**
在SpringBoot项目中,一般都会将这些公共组件封装为SpringBoot当中的starter,也就是我们所说的起步依赖。

**SpringBoot官方starter命名: spring-boot-starter-xxxx
第三组织提供的starter命名: xxxx-spring-boot-starter
**
在自定义一个起步依赖starter的时候,按照规范需要定义两个模块:

  1. starter模块(进行依赖管理[把程序开发所需要的依赖都定义在starter起步依赖中])
  2. autoconfigure模块(自动配置)
    实现
    第1步:创建自定义starter模块(进行依赖管理)
    把阿里云OSS所有的依赖统一管理起来
    第2步:创建autoconfigure模块
    在starter中引入autoconfigure (我们使用时只需要引入starter起步依赖即可)
    第3步:在autoconfigure中完成自动配置
  3. 定义一个自动配置类,在自动配置类中将所要配置的bean都提前配置好
  4. 定义配置文件,把自动配置类的全类名定义在配置文件中
    我们分析完自定义阿里云OSS自动配置的操作步骤了,下面我们就按照分析的步骤来实现自定义starter。
    aliyun-oss-spring-boot-starter模块

在这里插入图片描述
创建完starter模块后,删除多余的文件,最终保留内容如下:
删除pom.xml文件中多余的内容
在这里插入图片描述aliyun-oss-spring-boot-autoconfigure模块



创建完starter模块后,删除多余的文件,最终保留内容如下:


在starter模块中来引入autoconfigure这个模块的。打开starter模块中的pom文件:

<dependencies>
 <!--引入autoconfigure模块-->
 <dependency>
 <groupId>com.aliyun.oss</groupId>
 <artifactId>aliyun-oss-spring-bootautoconfigure</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 </dependency>
 
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter</artifactId>
 </dependency>
 </dependencies>

在autoconfigure模块当中来完成自动配置操作
下面我们就要定义一个自动配置类了,在自动配置类当中来声明AliOSSUtils的bean对象。

AliOSSAutoConfiguration类:

@Configuration//当前类为Spring配置类
@EnableConfigurationProperties(AliOSSProperties.class)//导入
AliOSSProperties类,并交给SpringIOC管理
public class AliOSSAutoConfiguration {
//创建AliOSSUtils对象,并交给SpringIOC容器
@Bean
public AliOSSUtils aliOSSUtils(AliOSSProperties
aliOSSProperties){
AliOSSUtils aliOSSUtils = new AliOSSUtils();
aliOSSUtils.setAliOSSProperties(aliOSSProperties);
return aliOSSUtils;
 }
}

AliOSSProperties类:

/*阿里云OSS相关配置*/
@Data
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
//区域
private String endpoint;
//身份ID
private String accessKeyId ;
//身份密钥
private String accessKeySecret ;
//存储空间
private String bucketName;
}

AliOSSUtils类:

@Data
public class AliOSSUtils {
private AliOSSProperties aliOSSProperties;
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws
IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 避免文件覆盖
String originalFilename =
multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() +
originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new
OSSClientBuilder().build(aliOSSProperties.getEndpoint(),
aliOSSProperties.getAccessKeyId(),
aliOSSProperties.getAccessKeySecret());
ossClient.putObject(aliOSSProperties.getBucketName(),
fileName, inputStream);
//文件访问路径
String url =aliOSSProperties.getEndpoint().split("//")[0] +
"//" + aliOSSProperties.getBucketName() + "." +
aliOSSProperties.getEndpoint().split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
 }
}

最后:在aliyun-oss-spring-boot-autoconfigure模块中的resources下,新建自动配置文件

Maven高级

Maven 是一款构建和管理 Java 项目的工具。
接下来还需要学习 Maven 提供的一些高级的功能,这些功能在构建和管理 Java
项目的时候用的也是非常多的。

分模块设计

概念和优势

所谓分模块设计,顾名思义指的就是我们在设计一个 Java 项目的时候,将一个 Java 项目拆分成多个模块进行开发。
项目不分模块,也就意味着所有的业务代码是不是都写在这一个 Java 项目当中。随着这个项目的业务扩张,项目当中的业务功能可能会越来越多
总结起来,主要两点问题:不方便项目的维护和管理、项目中的通用组件难以复用。

分模块设计就是将项目按照功能/结构拆分成若干个子模块,方便项目的管理维护、拓展,也方便模块键的相互调用、资源共享。
分模块开发需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然
后进行拆分。实际中都是分模块设计,然后再开发的。

实现

创建一个Maven模块,命名 项目名-pojo(这里以pojo为例)

在pojo中创建包,将原来的pojo实体类复制到该模块中

在pojo模块中引入模块所需依赖,并删除原来的工具类,在pom文件中引入pojo的依赖

总结
创建Maven模块,创建包并引入相关依赖,将该模块的依赖引入其他所需该模块的模块中。
1). 什么是分模块设计:将项目按照功能拆分成若干个子模块
2). 为什么要分模块设计:方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享
3). 注意事项:分模块设计需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分

继承

概念:继承描述的是两个工程间的关系,与java中的继承相似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承。
作用:简化依赖配置、统一管理依赖
实现

<parent>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<relativePath>....</relativePath>
</parent>

创建一个 parent 父工程,我们就可以将各个子工程当中共有的这部分依赖统一的定义在父工程 parent 当中,从而来简化子工程的依赖配置。
Maven不支持多继承,一个maven项目只能继承一个父工程,但是可以支持多重继承。


打包方式
pom:maven父类的打包方式必须是pom
该模块不写代码,仅进行依赖管理
war: 原始的spring,springmvc打包方式为war,必须部署在外部的tomcat服务器上运行。
jar:普通模块打包,springboot等打包方式为jar,可以使用内置的tomcat服务器运行,命令行jar -jar 文件名 即可运行。
具体实现
1.创建maven模块 tlias-parent ,该工程为父工程,设置打包方式pom(默认jar) 这个父类模块主要是管理依赖和配置的,不需要写代码

父工程tlias-parent的pom.xml文件配置如下:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itheima</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

2. 在子工程的pom.xml文件中,配置继承关系。

<parent>
<groupId>com.itheima</groupId>
<artifactId>tlias-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../tlias-parent/pom.xml</relativePath>
</parent>
<artifactId>tlias-utils</artifactId>
<version>1.0-SNAPSHOT</version>

其他的模块,都是相同的配置方式。
relativePath指定父工程的pom文件的相对位置(如果不指定,将从本地仓库/远程仓库查
找该工程,一般是第三方pom文件)。
3. 在父工程中配置各个工程共有的依赖(子工程会自动继承父工程的依赖)。

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>

注:实际开发中模块也会有分层结构

在真实的企业开发中,都是先设计好模块之后,再开始创建模块,开发项目。 那此时呢,
一般都会先创建父工程 tlias-parent,然后将创建的各个子模块,都放在父工程parent
下面。 这样层级结构会更加清晰一些。

版本锁定

在子工程中,配置了继承关系之后,坐标中的groupId是可以省略的,因为会自动继承父工
程的 。

一个项目中的多个模块,那多个模块中,我们要使用的同一个依赖的版本要一致,这样便于项目依赖的统一管理。
在maven中,可以在父工程的pom文件中通过 <dependencyManagement>来统一管理依赖版本。

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> 版本号了,
父工程统一管理。变更依赖版本,只需在父工程中统一变更。
我们之所以,在springboot项目中很多时候,引入依赖坐标,都不需要指定依赖的版本
<version>,是因为在父工程 spring-boot-starter-parent中已经通过
<dependencyManagement> 对依赖的版本进行了统一的管理维护。

属性配置实现版号管理

我们也可以通过自定义属性及属性引用的形式,在父工程中将依赖的版本号进行集中管理维护。 具体语法为:
自定义属性:

<properties>
 <lombok.version>1.18.24</lombok.version>
</properties>

引用属性

<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>
<jaxb.version>2.3.1</jaxb.version>
<activation.version>1.1.1</activation.version>
<jaxb.runtime.version>2.3.3</jaxb.runtime.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>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>${activation.version}</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.runtime.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

版本集中管理之后,我们要想修改依赖的版本,就只需要在父工程中自定义属性的位置,修改对应的属性值即可。
注:
是直接依赖,在父工程配置了依赖,子工程会直接继承下来。
是统一管理依赖版本,不会直接依赖,还需要在子工程中引入
所需依赖(无需指定版本)

聚合

分模块设计与开发之后啊,我们的项目被拆分为多个模块,而模块之间的关系,可能错综复杂。
由此,模块手动打包需要提前下载所需依赖的模块。

如果开发的是一个大型项目,要复杂的多。
聚合:将多个模块组织成一个整体,同时进行项目的构建。
聚合工程:一个不具有业务功能的“空”工程(有且仅有一个pom文件) 【一般来说,继承关系中的父工程与聚合关系中的聚合工程是同一个】
作用:快速构建项目(无需根据依赖关系手动构建,直接在聚合工程上构建即可)
实现
在maven中,我们可以在聚合工程中通过 <moudules> 设置当前聚合工程所包含的子模块的名称。我
们可以在 tlias-parent(一般在父类依赖管理的pom文件中写<moudules>标签)中,添加如下配置,来指定当前聚合工程,需要聚合的模块:

<!--聚合其他模块-->
<modules>
<module>../tlias-pojo</module>
<module>../tlias-utils</module>
<module>../tlias-web-management</module>
</modules>

那此时,我们要进行编译、打包、安装操作,就无需在每一个模块上操作了。只需要在聚合工程上,统一进行操作就可以了。
测试:执行在聚合工程 tlias-parent 中执行 package 打包指令那 tlias-parent 中所聚合的其他模块全部都会执行 package 指令,这就是通过聚合实现项目的一键构建(一键清理clean、一键编compile、一键测试test、一键打包package、一键安装install等)。

私服

**私服:**是一种特殊的远程仓库,它是架设在局域网内的仓库服务,用来代理位于外部的中央仓库,用于解决团队内部的资源共享与资源同步问题。
依赖查找顺序:
本地仓库
私服仓库
中央仓库
注意事项:私服在企业项目开发中,一个项目/公司,只需要一台即可(无需我们自己搭建,会使用即可)。
步骤分析
资源上传与下载,我们需要做三步配置,执行一条指令。
第一步配置:在maven的配置文件中配置访问私服的用户名、密码。(在自己maven安装目录下的conf/settings.xml中的servers中配
置)

<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin</password>
</server>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin</password>
</server>

第二步配置:在maven的配置文件中配置连接私服的地址(url地址)。(在自己maven安装目录下的conf/settings.xml中的mirrors、profiles中配置)

<mirror>
<id>maven-public</id>
<mirrorOf>*</mirrorOf>
<url>http://192.168.150.101:8081/repository/maven-public/</url>
</mirror>
1
2
3
4
5
<profile>
<id>allow-snapshots</id>
<activation>
 <activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>maven-public</id>
<url>http://192.168.150.101:8081/repository/mavenpublic/</url>
<releases>
 <enabled>true</enabled>
</releases>
<snapshots>
 <enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>

第三步配置:在项目的pom.xml文件中配置上传资源的位置(url地址)

<distributionManagement>
<!-- release版本的发布地址 -->
<repository>
<id>maven-releases</id>
<url>http://192.168.150.101:8081/repository/mavenreleases/</url>
</repository>
<!-- snapshot版本的发布地址 -->
<snapshotRepository>
<id>maven-snapshots</id>
<url>http://192.168.150.101:8081/repository/mavensnapshots/</url>
</snapshotRepository>
</distributionManagement>

配置好了上述三步之后,要上传资源到私服仓库,就执行执行maven生命周期:deploy。
私服仓库说明:
RELEASE:存储自己开发的RELEASE发布版本的资源。
SNAPSHOT:存储自己开发的SNAPSHOT发布版本的资源。
Central:存储的是从中央仓库下载下来的依赖。
项目版本说明:
RELEASE(发布版本):功能趋于稳定、当前更新停止,可以用于发行的版本,存储在私服中的RELEASE仓库中。
SNAPSHOT(快照版本):功能不稳定、尚处于开发中的版本,即快照版本,存储在私服的
SNAPSHOT仓库中。

  • 33
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值