JavaWeb后端阶段项目,从小白到高级开发,收藏这篇就足够了

项目工程

一、环境搭建

  • 准备数据库表(dept、emp)

  • 创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)

  • 配置文件application.properties中引入maybatis的配置信息,准备对应的实体类

  • 准备对应的Mapper、Service(接口、实现类)、Controller基础结构

deef0a76-e5cb-4ed5-866f-ced200259c1c

application.properties文件配置:

spring.application.name=Spring1
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=123456

#配置mybatis日志,指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#开启mybatis驼峰命名自动映射开关(例:a_column --->aColumn)
mybatis.configuration.map-underscore-to-camel-case=true
el-case=true


导入结构

controller(实体类)

mapper(接口)

pojo(实体类)

service(接口)

ae1f0cf7-ef46-483c-b888-077505e04052

配置xml映射文件,编写SQL语句(在resources文件夹下新建)

和Mapper接口名一样,在resources下创建包时不能用 “.” 来分层,只能用 “/”,但是显现出的效果相同

约束直接从官方文档拷贝

在Mybatis中文网进入“入门”,找到配置SQL语句的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="Mapper接口的全类名">

</mapper>

二、开发规范

273cc069-dc6a-46bf-80ce-9c09fe58b704

Restful特点

bcb1140d-657c-4b60-84c3-afd99f9070d1

  • url定位资源

  • HTTP动词描述操作

Rest是风格,是约定方式,约定不是规定,可以打破

描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:user、emps等

前后端交互统一响应结果Result:(放在pojo包下)

@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);
    }
}

三、前后端联调

  • 将资料中的文件压缩包拷贝到一个没有中文不带空格的目录下解压

  • 打开nginx.exe ,启动前端服务器(http://localhost:90/)

  • nginx会根据配置信息转到8080端口

  • 若tomcat端口号不是8080的,去nginx的config配置中把端口号改成要使用的端口号,并重新启动nginx

四、部门管理

(一)查询部门信息

在controller层下的DeptController类下编写

@Slf4j    //省略Logger对象
@RestController    //交给ioc容器管理,成为bean对象
public class DeptController {
    //@RequestMapping("/depts")     //通过其value属性来指定请求路径,method属性来指定请求方式,下面为简化版
    //有Get,Post,Put,Delete
    @GetMapping("/depts")
    public Result list() {
        log.info("查询全部数据");//输出日志
         //调用Service查询数据
        List<Dept> deptList = deptService.list();
        return Result.success(deptList);//传递返回的值
    }
}

====以下为SLf4j的源码====

Example: @Slf4j public class LogExample { }

will generate: public class LogExample { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class); }

在Service层的DeptService接口下创建 list() 方法,并在具体的实现类中重写 list() 方法,创建mapper对象并调用其方法、
接口

/**
* 部门管理
*/
public interface DeptService {
  /*
  * 查询全部部门数据*/
  List<Dept> list();
}

//

实现类

@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;//调用Dao层从数据库获取数据
@Override
public List<Dept> list() {
    return deptMapper.list();
}
}


在Dao层实现从数据库获取数据,用注解方式进行调用

/**
 * 部门管理
 */
@Mapper
public interface DeptMapper {
    /*
    * 查询全部部门方法
    * */
    @Select("select * from dept")
    List<Dept> list();
}



(二)删除部门

controller层:

/**
     * 删除部门
     * @return
     */
//    @DeleteMapping("/depts/{id}")
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id){
        //使用@PathVariable注解去获取@RequestMapping中{}中传进来的值,并绑定到处理方法定一的形参上
        //应用时,在@RequestMapping请求路径中,将需要传递的参数用花括号{}括起来,
        //然后通过@PathVariable("参数名称")获取URL中对应的参数值。
        log.info("根据id删除部门:{}",id);//一个大括号就是一个参数占位符
        deptService.delete(id);
        //调用Service删除部门
        return Result.success();
    }

service层
接口

/**
     * 根据id删除部门
     *
     * @param id
     */
    void delete(Integer id);
//

实现类

@Override
    public void delete(Integer id) {
        deptMapper.delete(id);
    }

dao层

@Delete("delete from mybatis.dept where id = #{id}")
    void delete(Integer id);

PS:postman测试时记得把请求方式换成DELETE

(三)新增部门
  • controller层

    /**
         * 新增部门
         */
        @PostMapping("/depts")
        public Result addDept(@RequestBody Dept dept){
            log.info("新增部门:{}",dept);
            //调用Service层新增
            deptService.addDept(dept);
            return Result.success();
        }
    
    
  • service层
    接口

    Dept addDept(Dept dept);
    
    

    实现类

    @Override
        public Dept addDept(Dept dept) {
            dept.setCreateTime(LocalDateTime.now());
            dept.setUpdateTime(LocalDateTime.now());//补全数据类型
            return deptMapper.addDept(dept);
        }
    
    
  • dao层

    /**
         * 新增部门
         * @param dept
         * @return
         */
        @Insert("INSERT INTO dept (name, create_time, update_time) VALUES (#{name},#{createTime},#{updateTime})")
        Dept addDept(Dept dept);
    
    

五、员工管理

基本数据封装对象:PageBean

package com.itheima.pojo;

import java.util.List;

public class PageBean {
    private Long total;
    private List rows;

    public Long getTotal() {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total;
    }

    public List getRows() {
        return rows;
    }

    public void setRows(List rows) {
        this.rows = rows;
    }
}


补充:

@RequestParam,@PathVariable注解的区别:

  1. @RequestParam
    从前端获取参数,其中的 defaultValue 可设置默认值

  2. @PathVariable
    通过请求路径指定参数,其中 defaultValue() 设置默认值

    请求路径:/depts/{id}
    
    
  3. @DateTimeFormat
    其中的pattern可以设置日期的格式,如

    @DateTimeFormat (pattern = "yyyy-MM-dd")
    
    
  4. @RequestBody
    @RequsetParam 可以接受post请求,但是请求的contentType 为 form-data格式,而@RequestBody接受的请求方式application/json;charset=UTF-8

(一)分页查询员工列表
1、原始方法

controller层

@Slf4j
//@RequestMapping("/emps")
@RestController
public class EmpController {
    @Autowired
    private EmpService empService;
    @GetMapping("/emps")
    public Result empSelect(@RequestParam(defaultValue = "1") Integer page,
                            @RequestParam(defaultValue = "10")Integer pageSize){
        //设置默认值
//        if(page==null)page = 1;
//        if(pageSize==null)pageSize=10;
        log.info("员工信息分页查询第{}页{}条",page,pageSize);
        PageBean bean = empService.empSelect(page,pageSize);
        return Result.success(bean);
    }
}

service层(在service层完成对数据的封装)

  • 接口

    public interface EmpService {
        PageBean empSelect(Integer page, Integer pageSize);
    }
    
    
  • 实体类

    public class EmpServiceImpl implements EmpService {
        @Autowired
        private EmpMapper empMapper;
        @Override
        public PageBean empSelect(Integer page, Integer pageSize) {
            PageBean bean = new PageBean();
            bean.setRows(empMapper.empSelect(page,pageSize)); //把从Dao层获取的数据封装到整合对象PageBean里
            bean.setTotal(empMapper.empTotal());
            return bean;
        }
    }
    
    

dao层

@Mapper
public interface EmpMapper {

    @Select("select *from emp limit #{page},#{pageSize}")
    public List<Emp> empSelect(Integer page, Integer pageSize);
    @Select("select count(*) from emp")
    public Long empTotal();
}


2、PageHelper插件
  1. 配置pom.xml文件

    <!--PageHelper分页插件-->
    <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.4.6</version>
            </dependency>
    
    
  2. EmpMapper

    /**
         * 员工信息查询
         * @return
         */
        @Select("select * from emp")
        public List<Emp> list();
    
    
  3. EmpServicelmp

    @Override
        public PageBean empSelect(Integer page, Integer pageSize) {
    
            //1.设置分页参数
            PageHelper.startPage(page,pageSize);
    
            //2.执行查询
            List<Emp> empList = empMapper.list();
            Page<Emp> p =(Page<Emp>) empList;//强制转换成Page对象
    
            //3.封装pagebean对象
            PageBean bean = new PageBean(p.getTotal(),p.getResult());
            return bean;
        }
    
    
3、条件分页查询(动态SQL)

SQL语句:

select * from emp where name like concat('%',?,'%') and gender = ? and entrydate between ? and ? order by update_time desc;

controller层

(补全参数)

public class EmpController {
    @Autowired
    private EmpService empService;
    @GetMapping("/emps")
    public Result empSelect(@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);
        PageBean bean = empService.empSelect(page,pageSize);
        return Result.success(bean);
    }
}

service层

(改造empSelect方法)

接口

public interface EmpService {
    PageBean empSelect(Integer page, Integer pageSize,String name, Short gender, LocalDate begin, LocalDate end);
}

实现类

@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageBean empSelect(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {

        //1.设置分页参数
        PageHelper.startPage(page,pageSize);

        //2.执行查询
        List<Emp> empList = empMapper.list(name,gender,begin ,end);
        Page<Emp> p =(Page<Emp>) empList;//强制转换成Page对象

        //3.封装pagebean对象
        PageBean bean = new PageBean(p.getTotal(),p.getResult());
        return bean;
    }
}

dao层

(改造list方法)

mapper

public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

EmpMapper映射文件

<?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">
                                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>

(二)批量删除员工数据

contraller

@DeleteMapping("/emps/{ids}")
    public Result Delete(@PathVariable ArrayList<Integer> ids){
        log.info("批量删除,ids:{}",ids);
        empService.Delete(ids);
        return Result.success();
    }

service

  • 接口

    void Delete(ArrayList ids);
    
    
  • 实现类

    @Override
        public void Delete(ArrayList ids) {
            empMapper.Delete(ids);
        }
    
    

dao

  • mapper

    void Delete(ArrayList ids);
    
    
  • xml映射文件

    <!--    批量删除操作-->
        <delete id="Delete">
            delete from emp where id in
            <foreach collection="ids" item="id" separator="," open="(" close=")">
                #{id}
            </foreach>
        </delete>
    
    
(三)新增员工数据

controller

@PostMapping("/emps")
    public Result save(@RequestBody Emp emp){
        log.info("新增员工,{}",emp);
        empService.Insert(emp);
        return Result.success();
    }

service

  • 接口

    void Insert(Emp emp);
    
    
  • 实现类(补全参数)

    @Override
        public void Insert(Emp emp) {
            emp.setCreateTime(LocalDateTime.now());//设置创建时间参数
            emp.setUpdateTime(LocalDateTime.now());//设置修改时间参数
            empMapper.Insert(emp);
        }
    
    

dao

@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);

(四)文件上传

将本地图片、视频、音频上传到服务器供其他用户浏览或下载

63be757e-7b0c-4c5f-b413-dd38fe4ca943

1.本地存储

代码实现:

  1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
  2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下

MultipartFile 常见方法:

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

把前端文件复制到resources文件夹下的static中

controller

package com.itheima.controller;

import com.itheima.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

@Slf4j   //日志注解
@RestController
public class UploadController {
    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        //也可以用@RequestParm注解指定对应参数,如:@RequestParm("image") MultipartFile file
        log.info("文件上传,{},{},{}",username,age,image);

        //1.获取原始文件名
        String imageName = image.getOriginalFilename();

        //2.构成新文件名
        int index = imageName.lastIndexOf('.');
        UUID uuid = UUID.randomUUID();
        String newName = uuid.toString()+imageName.substring(index);

        //3.将文件存储在服务器的磁盘目录中:E:\javaprogram\SaveImages
        image.transferTo(new File("E:\\javaprogram\\SaveImages\\"+newName));//该方法将接收到的文件转存到指定磁盘目录下
        log.info("新文件名,{}",newName);
        return Result.success();
    }
}


其中,文件默认不超过1M

在properties文件中修改限制

#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size = 10MB
#配置单个请求最大上传大小(一次请求能上传的总文件大小)
sprin.servlet.multipart.max-file-size = 100MB

2.阿里云存储OSS

ff446f8b-2631-46b7-a82b-cd19d9dee440

第三方服务思路

c4f1f9e8-50e3-437f-a08e-367496776efd

SDK:Software Development Kit,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

  1. 开通OSS服务之后,就可以进入到阿里云对象存储的控制台

image20220904201810832

  1. 点击左侧的 “Bucket列表”,创建一个Bucket

image20220904202235180

f09443ae-a505-435e-a9f8-9ab3984e14d5

AccessKey:

AccessKey ID:
AccessKey Secret:

入门程序

引入依赖项

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

上传文件流

以下代码用于将文件流上传到目标存储空间examplebucket中exampledir目录下的exampleobject.txt文件。

1.修改Endpoint

2.填写bucketName

3.指定保存至阿里云时的名称 objectName

4.指定将要上传的文件的本地路径 filePath

5.配置id和secret

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.File;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "examplebucket";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "exampledir/exampleobject.txt";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
        String filePath= "D:\\localpath\\examplefile.txt";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);

        try {
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath));
            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传文件。
            PutObjectResult result = ossClient.putObject(putObjectRequest);           
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

由于oss进行了更新,所以Id和Secret的配置参照该文章:

后端之路——阿里云OSS云存储-优快云博客

集成

UploadController

1.接收上传的图片

2.存储图片(OSS)

3.返回访问图片的url

  1. 引入阿里云OSS工具类(示例代码改)
    新建utils包

    package com.itheima.utils;
    
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClientBuilder;
    import com.aliyun.oss.common.auth.CredentialsProviderFactory;
    import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
    import org.springframework.web.multipart.MultipartFile;
    import java.io.*;
    import java.util.UUID;
    
    /**
     * 阿里云 OSS 工具类
     */
    @Component           //解释:utils不属于三层,交给IOC容器管理直接加@Component
    public class AliOSSUtils {
    
        private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        private String bucketName = "bucket122333";
    
        /**
         * 实现上传图片到OSS
         */
        public String upload(MultipartFile file) throws Exception {
            // 获取上传的文件的输入流
            InputStream inputStream = file.getInputStream();
    
            // 避免文件覆盖
            String originalFilename = file.getOriginalFilename();
            String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
    
            //上传文件到 OSS
            EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
            OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
            ossClient.putObject(bucketName, fileName, inputStream);
    
            //文件访问路径
            String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
            // 关闭ossClient
            ossClient.shutdown();
            return url;// 把上传到oss的路径返回
        }
    
    }
    
    
    
  2. 上传图片接口开发

controller

package com.itheima.controller;

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.File;
import java.io.IOException;
import java.util.UUID;

@Slf4j   //日志注解
@RestController
public class UploadController {
    @Autowired
    private AliOSSUtils aliOSSUtils;
    //                                  阿里云OSS存储
    @PostMapping("/upload")
    public Result upload(MultipartFile image) throws Exception {
        log.info("文件上传,文件名{}",image.getOriginalFilename());
        //调用阿里云OSS工具类进行文件上传
        String url = aliOSSUtils.upload(image);
        log.info("文件上传成功,文件url{}",url);
        return Result.success(url);
    }
}



(五)修改员工数据
1.回显

controller

@GetMapping("/{id}")
    public Result search(@PathVariable Integer id){
        log.info("根据id查询员工,id:{}",id);
        Emp emp = empService.Search(id);
        return Result.success(emp);
    }

service

  • 接口

    Emp Search(Integer id);
    
    
  • 实现类

    @Override
        public Emp Search(Integer id) {
            return empMapper.Search(id);
        }
    
    

dao

@Select("select * from emp where id = #{id}")
    Emp Search(Integer id);

2.修改数据

controller

@PutMapping
    public Result update(@RequestBody Emp emp){
        log.info("根据id修改员工数据,id:{}",emp.getId());
        empService.update(emp);
        return Result.success();
    }

service

  • 接口

    void update(Emp emp);
    
    
  • 实现类

    @Override
        public void update(Emp emp) {
            emp.setUpdateTime(LocalDateTime.now());
            empMapper.update(emp);
        }
    
    

dao

  • mapper

    void update(Emp emp);
    
    
  • 映射文件

    <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>
    
    

六、配置文件

(一)参数配置化(properties)

问题分析:AliOSSUtils文件下的第三方信息硬编码在代码中,不便于维护和管理

private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
//地址
private String bucketName = "bucket122333";
//库名

问题解决:在application.properties文件中配制阿里云信息

#自定义阿里云配置信息
aliyun.oss.endpoint = https://oss-cn-beijing.aliyuncs.com
aliyun.oss.bucketName = bucket122333

@Value 注解:用于外部配置的属性注入,具体用法如下

@Value("${配置文件中的key}")

@Component           //解释:utils不属于三层,交给IOC容器管理直接加@Component
public class AliOSSUtils {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";

    @Value("${aliyun.oss.bucketName}")
    private String bucketName = "bucket122333";


(二)yml配置文件

SpringBoot提供了多种属性配置方式

常见配置文件格式:

  • application.properties

    server.port = 8080
    server.address = 127.0.0.1
    
    
  • application.yml / yaml(冒号后要加上空格,每一级用两个空格缩进)

    server:
     port: 8080
     address: 127.0.0.1
    
    
  • xml

    <server>
        <port>8080</port>
        <address>127.0.0.1</address>
    </server>
    
    

yml基本语法

  • 大小写分明

  • 数值前必须有空格作为分隔符

  • 使用缩进表示层级关系,缩进时不允许使用tab键,只能用空格

  • 缩进的数目不重要,只要相同层级的元素左侧对其即可

  • #表示注释

e08d01a5-26a4-4bf8-919e-77f0f891b1b2

yml和properties对比

spring:
  application:
    name: Spring1
    #数据库连接信息
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis
    username: root
    password: 123456   #密码无法连接数据库时用单引号引起来
    #文件上传配置
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB
#Mybatis配置
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
#阿里云OSS配置
aliyun:
  oss:
    bucketName: bucket122333
    endpoint: https://oss-cn-beijing.aliyuncs.com

spring.application.name=Spring1
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=123456

#配置mybatis日志,指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#开启mybatis驼峰命名自动映射开关(例:a_column --->aColumn)
mybatis.configuration.map-underscore-to-camel-case=true
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size = 10MB
#配置单个请求最大上传大小(一次请求能上传的总文件大小)
spring.servlet.multipart.max-request-size = 100MB

#配置阿里云OSS
aliyun.oss.endpoint = https://oss-cn-beijing.aliyuncs.com
aliyun.oss.bucketName = bucket122333

(三)@ConfigurationProperties

可以将配置文件中配置项的值自动注入到对象的属性中(取代@Value)

前提条件:

  • 需要注入的属性有GET和SET方法(@Data)

  • 属性名要一致,前缀名一致

  • 需要该类由IOC容器管理(@Component)

40525530-033e-4b1c-aa49-36be7840cec0

@Component           //解释:utils不属于三层,交给IOC容器管理直接加@Component
@Data
@ConfigurationProperties(prefix = "aliyun.oss" )
public class AliOSSUtils {

//    @Value("${aliyun.oss.endpoint}")
    private String endpoint ;
//    @Value("${aliyun.oss.bucketName}")
    private String bucketName ;

可选操作:解决报红

引入configuration依赖,自动识别被@ConfigurationProperties注解标识的bean对象,在配置文件中配置时自动提示与该bean对象属性名相对应的配置项名字

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

如图所示

cdf2e128-a965-4a85-86f0-f770f7544f2a

@ConfigurationProperties 和 @Value 的对比

  • @Value注解只能一个一个的进行外部属性的注入

  • @ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中

七、基础登录取功能

(一)登录基本操作

controller(其中判断用户名和密码是否正确的

操作放在该层)

@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;
    @PostMapping
    public Result login(@RequestBody Emp emp){
        log.info("员工登录:{}",emp);
        Emp e =  empService.login(emp);
        if(e!=null){
            return Result.success();
        }else {
            return Result.error("用户名或密码错误");
        }
    }
}

service

  • 接口

    Emp login(Emp emp);
    
    
  • 实现类

    /**
         * 员工登录
         */
        @Override
        public Emp login(Emp emp) {
            return empMapper.getByUsernameAndPassword(emp);
        }
    
    

dao

/**
     * 根据用户名和密码查询员工
     * @param emp
     * @return
     */
    @Select("select * from emp where username = #{username} and password = #{password}")
    Emp getByUsernameAndPassword(Emp emp);

(二)会话技术
  • 会话:用户打开浏览器,访问web服务器资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应

  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

  • 会话跟踪方案:
    1.客户端会话跟踪技术:Cookie
    2.服务端会话跟踪技术:Session
    3.令牌技术
    a65fcd98-b1f5-4749-93ce-7f4a26a44086

(1)Cookie

1.Cookie:HTTP请求头包含存储先前通过与所述服务器发送的HTTP cookies Set-Cookie 头

2.Set-Cookie:所述Set-Cookie HTTP响应头被用于从服务器向代理发送 cookie

3903d776-ea06-472b-83e6-b807ac805dc7

存储在浏览器本地,请求之后返回cookie值,下次访问时从浏览器本地发送cookie

(2)Sessions

825899ca-79c0-4f1f-8b6e-6ae1b3b4366c

存储在服务器端,请求后从服务端查询是否存在相应值,但是服务器集群无法使用

(3)JWT令牌(主流)
  • 全称:JSON Web Token

  • 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的

  • 组成

    • 第一部分:Header(头),记录令牌类型、签名算法等。例如:{“alg”:“HS256” , “type”:“JWT”}

    • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{“id”:“1” , “username”:“Tom”}

    • 第三部分:Signature(签名),防止Token被纂改、确保安全性。将header、payload,并加入指定密钥,通过指定签名算法计算而来

e355b87c-4d13-4615-bf33-4a5305bf8e4c

  • 使用场景:登录验证

    1. 登录成功后生成令牌

    2. 后续每个请求都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后再处理

e842f9ca-4678-4bf2-9873-bb78726da866
配置xml文件

<!--JWT令牌-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

在测试类中添加测试方法生成令牌

(要是报错ClassNotFoundException,添加依赖jaxb-api)

<!--jaxb-api-->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>

  • 生成令牌
    @Test      //生成JWT令牌
    public void testGenJwt(){
        Map<String, Object> claims = new HashMap<>();
        claims.put("id",1);
        claims.put("name","tom");

        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "itheima")//用于设置数字签名的算法,例如:HS256(256位密钥)
                .setClaims(claims)//自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1小时
                .compact();//获取成字符串
        System.out.println(jwt);
    }

  • 解析令牌
    @Test
    public void testParseJWT(){
        Map<String, Object> claims = Jwts.parser()
                .setSigningKey("itheima")//指定签名密钥
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcyOTQzMTk0MX0.uZw_C-9qhHFakqoLyG3Jh-MRQrwSmHCN2kfgQBdUhSY")//解析生成的jwt令牌(2024/10/20过期)
                .getBody();
        System.out.println(claims);
    }


  • jwt校验时使用的签名密钥,必须和生成jwt令牌时使用的密钥是配套的
  • 如果jwt令牌解析校验时报错,则说明jwt令牌被 篡改失效 了,令牌非法
(三)使用JWT令牌完成登录校验

思路

  • 令牌生成:登录成功后,生成JWT令牌,并返回给前端
  • 令牌校验:在请求到达服务端后,对令牌进行统一拦截、校验

1.引入JWT令牌工具类

package com.itheima.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;

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)
                .getBody();
        return claims;
    }
}


2.修改登录controller代码

@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("员工登录:{}",emp);
        Emp e =  empService.login(emp);
        if(e!=null){
            //登录成功,生成令牌,下发令牌
            Map<String,Object> jwt = new HashMap<>();
            //在jwt令牌生成中加上ID,用户名,姓名
            jwt.put("ID",e.getId());
            jwt.put("username",e.getUsername());
            jwt.put("Name",e.getName());
            String s = JwtUtils.generateJwt(jwt);
            return Result.success(s);
        }else {
            //登录失败,返回
            return Result.error("用户名或密码错误");
        }
    }
}

(四)统一拦截
(1)Filter过滤器
  • 概念:是javaweb三大组件(Servlet、Filter、Listener)之一
  • 过滤器可以把对资源的请求拦截下来

基本操作:

  1. 定义Filter:定义一个类实现Filter接口

    PS:导入的Filter是javax.servlet.Filter

  2. 配置Filter:在Filter类上加上@WebFilter注解,配置拦截资源的路径(urlPatterns),启动类加上@ServletComponentScan开启Servlet组件支持

    定义和配置Filter:

    package com.itheima.Filter;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import java.io.IOException;
    
    @WebFilter(urlPatterns = "/*")      //拦截所有请求
    public class DemoFilter implements Filter {
    
        @Override    //初始化方法,只调用一次,默认实现
        public void init(FilterConfig filterConfig) throws ServletException {
            //System.out.println("init初始化方法执行了");
            Filter.super.init(filterConfig);
        }
    
        @Override  //拦截到请求都会调用
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("拦截到了请求");
        }
    
        @Override     //销毁方法,只调用一次,默认实现
        public void destroy() {
            //System.out.println("destroy销毁方法执行了");
            Filter.super.destroy();
        }
    }
    
    
    

    配置启动类:

    @ServletComponentScan   //配置Servlet组件支持
    @SpringBootApplication
    public class Spring1Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Spring1Application.class, args);
        }
    
    }
    
    

过滤器执行流程:

  • 放行前逻辑
  • 放行(放行后访问对应资源)
  • 放行后逻辑(回到Filter但是执行放行之后的代码)

Filter的拦截路径:可以根据需求配置不同的拦截资源路径

拦截路径urlPatterns值含义
拦截具体路径/login只有访问/login路径时,才会被拦截
目录拦截/emps/*访问/emps下的所有资源时,会被拦截
拦截所有/*访问所有资源,都会被拦截

过滤器链:一个web应用中可以配置多个过滤器,形成一个过滤器链。

访问流程:filter1放行前–》放行–》filter2放行前–》放行–》访问资源–》filter2放行后–》filter1放行后

abc过滤器(" /* ")

@WebFilter(urlPatterns = "/*")
public class AbcFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("abc拦截到了请求");
        //放行
        filterChain.doFilter(servletRequest,servletResponse);

        System.out.println("abc放行后执行逻辑");
    }
}


demo过滤器(" /login ")

@WebFilter(urlPatterns = "/login")//拦截所有请求
public class DemoFilter implements Filter {
    @Override  //拦截到请求都会调用
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("demo拦截到了请求");
        //放行
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("demoF拦截请求放行");
    }
}


image-20241120214913039

顺序:注解配置的Filter,优先级按照过滤器类名(字符串)的自然排序

登录校验功能实现:

流程图:image-20241120220345392

步骤:

  • 获取url
  • 判断请求url中是否包含login,如果包含,放行
  • 获取请求头中令牌
  • 判断令牌是否存在,如果不存在返回错误结果
  • 解析token,如果解析失败返回错误结果
  • 放行

阿里巴巴fastjosn:手动把Result转换成json数据(版本号改成2.0.51可用)

<!--阿里巴巴fastjosn-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            
        </dependency>

代码实现:

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;//强转为http获取请求参数
        HttpServletResponse resp = (HttpServletResponse) servletResponse;//强转为http获取响应参数
                //> 1- 获取url
        String url = req.getRequestURL().toString();
        log.info("请求的url:{}",url);
                //> 2- 判断请求url中是否包含login,如果包含,放行
        if(url.contains("login")){
            log.info("登录操作,放行");
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
                //> 3- 获取请求头(token)中令牌     见api文件
        String jwt = req.getHeader("token");

                //> 4- 判断令牌是否存在,如果不存在返回错误结果
        if(!StringUtils.hasLength(jwt)){
            log.info("令牌为空,失败");
            Result notLogin = Result.error("NOT_LOGIN");
            //手动转化  对象--JSON-------》阿里fastJOSN
//            这里的JSONObject是com.alibaba.fastjson.JSONObject
            String notl = JSONObject.toJSONString(notLogin);
            resp.getWriter().write(notl);//直接响应给浏览器
            return;
        }
                //> 5- 解析token,如果解析失败返回错误结果
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) { //解析失败
            e.printStackTrace();
            log.info("jwt令牌解析失败");
            Result notLogin = Result.error("NOT_LOGIN");
            String notl = JSONObject.toJSONString(notLogin);
            resp.getWriter().write(notl);//直接响应给浏览器
            return;
        }
        //> 6- 放行
        log.info("令牌有效,放行");
        filterChain.doFilter(servletRequest,servletResponse);
    }
}


(2)Interceptor拦截器

基本操作:

  1. 定义拦截器:定义一个类实现Interceptor接口

  2. 注册拦截器

定义拦截器

@Component //交给ioc容器管理
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///////");
    }

    @Override //视图渲染完毕后运行,最后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion........");
    }
}

注册拦截器(新建配置类包)

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor interceptor;
    @Override  //注册拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns("/login")
                                             //需要拦截的资源          //不需要拦截的资源
    }
}


拦截路径含义举例
/*一级路径能匹配/depts,/emps,/login,不能匹配/depts/1
/**任意级路径能匹配任意级路径
/depts/*/depts下的一级路径能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**/depts下的任意级路径能匹配/depts/1,/depts/1/2,/depts,不能匹配/emps/1

拦截器、过滤器执行流程:image-20241124221709018

拦截器和过滤器之间的区别:

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口
  • 拦截范围不同:过滤器Filter会拦截所有资源,而Interceptor只会拦截Spring环境中的资源

八、异常处理

异常抛出过程:

image-20241128213527169

  • 全局异常处理器
    设置全局异常处理器
    /**
     * 全局异常处理器
     */
    @RestControllerAdvice
    //@RestControllerAdvice = @ControllerAdvice + @ResponseBody
    public class GlobalExceptionHandler {
        @ExceptionHandler(Exception.class)//捕获所有异常
        public Result ex(Exception ex){
            ex.printStackTrace();
            return Result.error("对不起,操作失败,请联系管理员");
        }
    }
    
    

九、事务管理

(一)回顾事务

概念:事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败

操作

  • 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  • 提交事务(这组操作全部成功后,提交事务):commit ;
  • 回滚事务(中间出现任何异常,回滚事务):rollback ;

完善删除部门操作

@Override
    public void delete(Integer id) {
        //1.删除部门
        deptMapper.delete(id);
        //2.根据部门id删除部门下的员工信息
        empMapper.DeleteByDeptId(id);
    }


/**
     * 根据部门id删除部门下员工
     * @param id
     */
    @Delete("delete from emp where dept_id = #{id}")
    void DeleteByDeptId(Integer id);
}

(二)Spring事务管理
  • 事务注解:@Transactional
  • 位置:业务(service)层的方法上、类上、接口上
  • 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务

执行多次数据访问操作的方法上添加@Transactional注解

开启事务管理日志:

logging:
 level:
  org.springframework.jdbc.support.JdbcTransactionManager:debug

 @Transactional//事务注解
    @Override
    public void delete(Integer id) {
        //1.删除部门
        deptMapper.delete(id);
        int i = 1/0;//模拟异常
        //2.根据部门id删除部门下的员工信息
        empMapper.DeleteByDeptId(id);
    }

(三)事务进阶
1.rollbackFor

默认情况下,只有出现RuntimeException才会回滚异常。rollbackFor属性用于控制出现何种异常,回滚事务

设置@Transactional的rollbackFor值

//出现任何异常都回滚
    @Transactional(rollbackFor = Exception.class)//事务注解
    @Override
    public void delete(Integer id) {
        //1.删除部门
        deptMapper.delete(id);
//        int i = 1/0;//模拟异常
        //2.根据部门id删除部门下的员工信息
        empMapper.DeleteByDeptId(id);
    }

2.propagation(事务传播行为)

事务的传播行为:指的是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制

例如

//a方法
@Transactional
public void a(){
  //...
  userService.b();
  //...
}

//b方法
@Transactional
public void b(){
    //...
}

此时的b方法是否需要开启一个新的事务?

事务的常见的传播行为

属性值含义
REQUIRED【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在无事务的状态中运行
NOT_SUPPORTED不支持事务,在无事务的状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY必须有事务,否则抛出异常
NEVER必须没事务,否则抛出异常

补充代码:

  • 日志类

    package com.itheima.pojo;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.time.LocalDateTime;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class DeptLog {
        private Integer id;
        private LocalDateTime createTime;
        private String description;
    }
    
    
    
  • 日志表的创建

    create table dept_log(
       	id int auto_increment comment '主键ID' primary key,
        create_time datetime null comment '操作时间',
        description varchar(300) null comment '操作描述'
    )comment '部门操作日志表';
    
    
  • DeptLogService 接口

    package com.itheima.service;
    import com.itheima.pojo.DeptLog;
    
    public interface DeptLogService {
        void insert(DeptLog deptLog);
    }
    
    
    
  • DeptLogServiceImpl 实体类

    package com.itheima.service.impl;
    import com.itheima.mapper.DeptLogMapper;
    import com.itheima.pojo.DeptLog;
    import com.itheima.service.DeptLogService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class DeptLogServiceImpl implements DeptLogService {
    
        @Autowired
        private DeptLogMapper deptLogMapper;
    
        @Transactional//(propagation = Propagation.REQUIRES_NEW)
        @Override
        public void insert(DeptLog deptLog) {
            deptLogMapper.insert(deptLog);
        }
    }
    
    
    
  • DeptLogMapper

    package com.itheima.mapper;
    import com.itheima.pojo.DeptLog;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface DeptLogMapper {
    
        @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
        void insert(DeptLog log);
    
    }
    
    
    

解散部门

需求:解散部门时,无论是成功还是失败,都要记录操作日志

步骤:

  1. 解散部门:删除部门、删除部门下的员工
  2. 记录日志到数据库表中

(1)记录日志

@Transactional(rollbackFor = Exception.class)//事务注解
    @Override
    public void delete(Integer id) {
        //1.删除部门
        deptMapper.delete(id);
//        int i = 1/0;//模拟异常
        //2.根据部门id删除部门下的员工信息
        empMapper.DeleteByDeptId(id);

        //记录解散部门的操作
        DeptLog deptLog = new DeptLog();
        deptLog.setCreateTime(LocalDateTime.now());
        deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
        deptLogService.insert(deptLog);

    }

(2)根据需求优化代码(try-catch-finally)

 @Transactional(rollbackFor = Exception.class)//事务注解
    @Override
    public void delete(Integer id) {
        //1.删除部门
        try {
            deptMapper.delete(id);
//        int i = 1/0;//模拟异常
            //2.根据部门id删除部门下的员工信息
            empMapper.DeleteByDeptId(id);
        } finally {
            //记录解散部门的操作
            DeptLog deptLog = new DeptLog();
            deptLog.setCreateTime(LocalDateTime.now());
            deptLog.setDescription("执行了解散部门的操作,此次解散的是" + id + "号部门");
            deptLogService.insert(deptLog);
        }

    }

由于默认值使得insert方法中的事务传播行为是 加入已有事务,所以当删除部门报错后,记录日志的操作会被一同回滚。

(3)修改 DeptLogServiceImpl 类下的 insert 方法的事务传播行为 (**REQUIRES_NEW**新建一个事务)

@Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }

十、AOP基础

概念:面向切面编程、面向方向编程,其实就是面向特定方法编程。

场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时

  1. 获取方法开始时间
  2. 运行原始方法
  3. 获取方法运行结束时间,计算执行耗时

实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的都太代理机制,对特定的方法进行编程。

(一)快速入门:

统计各个业务层方法执行耗时

  1. 导入依赖:在pom.xml中导入AOP的依赖

    		<!--AOP依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
    
  2. 编写AOP程序:针对于特定方法根据业务需要进行编程

    package com.itheima.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;
    
    @Slf4j
    @Component
    @Aspect
    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);//getSignature可以拿到方法名
    
            return result;
        }
    }
    
    
    

    AOP应用场景:记录操作日志、权限控制、事务管理等

    优势:代码无侵入、减少重复代码、提高开发效率、维护方便

(二)核心概念
  • 连接点:JoinPoint,可以呗AOP控制的方法(暗含方法执行时的相关信息)
  • 通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象

image-20241217192444046

执行过程:程序运行时会自动为目标对象生成一个代理对象,在代理对象中对目标对象的功能进行增强(在不修改源代码的前提下增加了功能),即通知内的代码进行拼接,最后注入的时候不再注入目标对象而是代理对象,调用方法的时候调用的则是代理对象的方法

(三)通知类型以及顺序
各种通知类型
  1. @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  2. @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  3. @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  4. @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  5. @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

正常情况下采用

@Before("execution(路径)")
    public void before(JoinPoint joinPoint){
        log.info("before ...");

    }

公共表达式@Pointcut

其中,Spring中提供了@Pointcut注解,即公共切入点表达式,用新构造的空方法替代execution,如下所示

@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 ...");
    }
}

也可以修改公共切入点表达式方法的属性(如private改成public),便可以在其他aop类下使用该表达式的路径

通知顺序

默认情况下,当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行,并且按照切面类类名的字母顺序,before字母排名靠前的先执行,after字母排名靠后的先执行

Spring提供了@Order( )注解加在切面类上来控制顺序

  • 目标方法前的通知:数字小的先执行
  • 目标方法后的通知:数字大的先执行
(四)切入点表达式

常见形式:

(1)execution(…)

根据方法的签名来匹配

格式:execution(访问修饰符 返回值 包名.类名.方法名(方法参数) throws 异常)

  • 访问修饰符(可省略)
  • 包名.类名(可省略,但是不建议省略)
  • throws 异常(可省略,注意是方法上声明抛出的异常,不是实际抛出的异常)
@Before("execution(public void com.itherma.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint){
    
}

通配符号:

  • *:表示单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,亦可以通配包、类、方法名的一部分

    execution(* com.*.service.*.update*(*))
    
    

    示例:

    execution(* com.itheima.service.*Service.delete*(*))
    
    

    表示的是在com.itheima.service下的任意返回值的以Service结尾的包下的以delete开头的内含一个任意类型参数的方法

  • …:表示多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

    execution(* com.itheima..DeptService.*(..))
    
    

同时匹配两个方法:

这里需要匹配的是DeptService下的两个方法

List<Dept> list();
和
void delete(Integer id);

@Pointcut("execution(* com.itheima.service.DeptService.list()) ||" + 
		"execution(* com.itheima.service.DeptService.delete())")
private void pointcut(){
    
}

(2)@annotation(…)

根据注释匹配,用于匹配标识有特定注解的方法

@Before("@annotation(com.itheima.anno.Log)")
public void before(){

}

步骤:

  1. 自定义注解Log

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)//作用于方法上
    public @interface MyLog {
    }
    
    
  2. 在对应方法上加上自定义注解(起到标识作用)

    	@Log
        @Override
        public List<Dept> list() {
            List<Dept> deptList = deptMapper.list();
            return deptList;
        }
        @Log
        @Override
        public void delete(Integer id) {
            deptMapper.delete(id);
        }
    
    
  3. 改写切入点表达式

    匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
        //@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
        @Pointcut("@annotation(com.itheima.aop.Log)")
        private void pt(){}
    
    
注解和反射(补充内容,之前学得不深入)

基础格式:

@元注解
public @interface Test{
    定义内容
}

(一)元注解负责解释自定义注解,决定了我们自定义注解的应用范围,java定义了4个标准的meta-annotation类型

  1. @Target({ElementType.xxx, ElementType.xxx})

    表示自定义注解的作用范围

    • TYPE表示当前自定义注解可以添加在类,接口,枚举类上
    • FIELD表示可以添加在字段属性上
    • METHOD表示可以添加在方法上
    • PARAMETER表示可以添加到参数上
    • CONSTRUCTOR表示可以添加到构造器上
  2. @Retention(RetentionPolicy.xxx)

    表示自定义注解的生命周期

    • SOURCE表示只存在于源代码Java文件中
    • CLASS表示会在class文件中保留但是在jvm加载运行的时候消失
    • RUNTIME表示存在于运行阶段(通常使用的自定义注解使用的都是RUNTIME)
  3. @Documented

    说明该注解将被包含在javadoc中

  4. @Inherited

    表示子类可以继承父类中的该注解

(二)定义内容

  1. 其中的每一个方法实际上是声明了一个配置参数
  2. 方法的名称就是参数的名称
  3. 返回值类型就是参数的类型
  4. 可以通过default来声明参数的默认值
  5. 如果只有一个成员参数一般参数名为value
//自定义注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation{
	//注解的参数:参数类型 + 参数名();
    String name() default "";
    int age() default 0;
    int id() default -1;  //-1表示不存在
}

//使用该注解的类/方法
public class test{
    //注解可以显示赋值,若没有默认值,我们就必须给注解赋值
	@MyAnnotation(name = "小明")
	public void test(){
	}
}

(三)反射(获取注解的途径)

Java反射机制的反射类:Class,Method,Field,Constructor

常用方法getClass,getinterfaces,getMethods,getDeclaredField,getDeclaredConstructor,getAnnotarion,Class.forName(“字符串”),newInstance

用反射操作

public class T {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public T() {
    }

    public T(int age) {
        this.age = age;
    }

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        Class c1 = Class.forName("com.test53_Annotation.T");
        //通过无参构造器(本质)创建对象
        T my1 =(T) c1.newInstance();
        System.out.println(my1);
        //通过反射获取构造器并创建对象
        Constructor constructor= c1.getDeclaredConstructor(int.class);
        T my2 = (T) constructor.newInstance(10);
        System.out.println(my2);
        //通过反射获取方法
        T my3 =(T) c1.newInstance();
        Method MYmethod = c1.getMethod("setAge", int.class);
                //invoke:激活
                //给方法传递(对象,“方法的值”)没有值填null
        MYmethod.invoke(my3,10086);
        System.out.println(my3.getAge());
        //通过反射操作属性
        T my4 = (T)c1.newInstance();
        Field MYage = c1.getDeclaredField("age");
        MYage.setAccessible(true);//关闭权限检测,可以访问private修饰的字段,其他class,method同理
        MYage.set(my4,999);
        System.out.println(my4.age);
    }
}


通过反射获取注解信息

import lombok.Data;

import java.lang.annotation.*;
import java.lang.reflect.Field;

public class Test1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        //获取类的对象
        Class c1 = Class.forName("com.test53_Annotation.Student");
        Annotation[] annotations = c1.getAnnotations();
        //通过反射获取注解
        for (Annotation a:annotations
             ) {
            System.out.println(a);
        }
        //通过反射获取Table注解的值
        Table table = (Table) c1.getAnnotation(Table.class);
        System.out.println(table.value());
        //通过反射获取Field注解的值
        Field f1 = c1.getDeclaredField("name");
        f1.setAccessible(true);//修改权限
        MField mf = f1.getAnnotation(MField.class);
        System.out.println(mf.columnName()+" "+mf.type()+" "+mf.length());
    }
}
@Data
@Table(value = "db_mybatis")
class Student{
    @MField(columnName = "id",type = "int",length = 10)
    private int id;
    @MField(columnName = "age",type = "int",length = 3)
    private int age;
    @MField(columnName = "name",type = "varchar",length = 5)
    private String name;

    public Student(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }
    public Student(){

    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Table{
    String value();//表名
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MField {
    String columnName();//列名
    String type();//类型
    int length();//长度
}

(五)连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名,方法名,方法参数等

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类(导入包的时候注意是org.aspectj.lang下的JoinPoint)
//公共切入点表达式
@Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    private void pt(){}
@Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("MyAspect8 ... before ...");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("MyAspect8 around before ...");

        //1. 获取 目标对象的类名 .
        String className = joinPoint.getTarget().getClass().getName();//Target:目标
        log.info("目标对象的类名:{}", className);

        //2. 获取 目标方法的方法名 .
        String methodName = joinPoint.getSignature().getName();//Signature:全称是Method Signature,方法签名,包括方法名称、参数列表等
        log.info("目标方法的方法名: {}",methodName);

        //3. 获取 目标方法运行时传入的参数 .
        Object[] args = joinPoint.getArgs();//Args:Arguments的缩写,方法参数
        log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

        //4. 放行 目标方法执行 .
        Object result = joinPoint.proceed();

        //5. 获取 目标方法运行的返回值 .
        log.info("目标方法运行的返回值: {}",result);

        log.info("MyAspect8 around after ...");
        return result;
    }

(六)AOP记录操作日志

将案例中增删改相关接口的操作日志记录到数据库中

包括操作人,操作时间,执行方法的全类名,执行方法名,方法运行时参数,返回值,方法执行时长

具体步骤:

  1. 创建自定义注解类 @Log

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Log {
    }
    
    
  2. 创建AOP切面类

    @Component
    @Slf4j
    @Aspect
    public class LogAspect {
        @Autowired
        private HttpServletRequest httpServletRequest;
        //HTTPServletRequest是Tomcat收到请求后创建的,
        // 然后传给Spring的Servlet
        @Autowired
        private OperateLogMapper operateLogMapper;
        @Pointcut("@annotation(com.itheima.anno.Log)")//注解配置切入点表达式
        public void pt(){
        }
        @Around("pt()")//使用环绕通知
        public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
            //获取操作人id,即获取jwt令牌
            String jwt = httpServletRequest.getHeader("token");
            Claims claims = JwtUtils.parseJWT(jwt);
            Integer operateUser =(Integer) claims.get("ID");
    
            //获取操作时间
            LocalDateTime operateTime = LocalDateTime.now();
            //操作类名
            String className = joinPoint.getTarget().getClass().getName();
            //操作方法名
            String methodName = joinPoint.getSignature().getName();
            //操作方法参数
            Object[] args = joinPoint.getArgs();
            String methodParams = Arrays.toString(args);//把集合元素变成字符串
          
            //操作耗时
            //记录开始时间
            long begin = System.currentTimeMillis();
            //调用原始方法运行
            Object result = joinPoint.proceed();
            //方法返回值
            String returnValue = JSONObject.toJSONString(result);
            //记录结束时间,计算方法执行耗时
            long end  = System.currentTimeMillis();
            Long costTime = end - begin;
            //记录操作日志
            OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
            operateLogMapper.insert(operateLog);
    
            log.info("AOP记录操作日志:{}",operateLog);
    
            return result;
        }
    }
    
    
  3. 在对应增删改方法上加上@Log注解

十一、SpringBoot底层原理讲解

(一)配置优先级
  • springboot中提供了properties,yaml,yml三种配置文件格式,当三个文件都配置了同一个属性时,优先级properties>yml>yaml

  • springboot除了支持配置文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置

    1. java系统属性

      -Dserver.port = 9000
      
      
    2. 命令行参数(优先级最高)

      --server.port = 10010
      
      

      image-20250118104748096

      image-20250118105335257

(二)bean的管理
  • 主动获取bean对象

    • 根据name获取bean对象

      Object getBean(String name)
      
      
    • 根据类型获取bean

      <T> T getBean(Class<T> requiredType)
      
      
    • 根据name获取bean(带类型转换)

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

      在测试类中自动注入ApplicationContext对象,即IOC容器对象,调用其getBean方法获取bean对象

  • bean的作用域

    可以通过@Scope注解来进行作用域的配置

    @Scope("prototype")
    @RestController
    @RequestMapping("/depts")
    public class DeptController{
    }
    
    

    默认初始化时期是在容器启动的时候,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)

    Spring支持五种作用域,后三种在web环境下才生效:

    作用域说明
    singleton容器内同名称的bean只有一个实例(单例)(默认
    prototype每次使用bean时都会创建新的实例(非单例)
    request每个请求范围中都会创建新的实例
    session每个会话范围内都会创建新的实例
    application每个应用范围内会创建新的实例
(三)第三方bean

因为第三方文件是只读文件,所以不能直接在类上加上@Component注解,非自定义注解就需要用@Bean注解

  • 在启动类中的方法加上@Bean注解将方法返回值交给IOC容器管理,成为IOC容器的bean对象(不推荐

    @SpringBootApplication
    public class SpringbootApplication{
    	@Bean //此注解会将方法返回值交给IOC容器管理,成为IOC容器的bean对象
        public SAXReader saxReader(){ //声明第三方bean
    		return new SAXReader();
        }
    }
    
    
  • 通过@Configuration注解声明一个配置类(CommonConfig),实现对这些bean进行集中管理(推荐

    @Configuration
    public class CommonConfig{
    	@Bean //此注解会将方法返回值交给IOC容器管理,成为IOC容器的bean对象
        public SAXReader saxReader(){ //声明第三方bean
    		return new SAXReader();
        }
    }
    
    
  • 如果第三方bean需要依赖其他bean对象,可以在bean定义方法中设置形参进行依赖注入

    @Configuration
    public class CommonConfig{
    	@Bean //此注解会将方法返回值交给IOC容器管理,成为IOC容器的bean对象
        	  //可以通过@Bean注解的name/value属性指定bean的名称
        public SAXReader saxReader(DeptService deptService){ //注入对应方法形参,spring容器会进行自动装配
             System.out.println(deptService);
    		return new SAXReader();
        }
    }
    
    
(四)SpringBoot原理

spring基本框架是Spring Framework,但是其配置、依赖繁琐,所以官方推出了springboot框架来简化基于Spring框架的开发。

SpringBoot提供了起步依赖,自动配置功能则简化了框架在使用时bean的声明、bean的配置

1. 起步依赖

image-20250121222328242

而在SpringBoot依赖中只需要引入一个依赖

image-20250121223525193

原理就是maven的依赖传递

2. 自动配置

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

  • 方案一:@ComponentScan 组件扫描(引入第三方依赖)

    @ComponentScan({"com.example","com.itheima"})
    @SpringBootApplication
    public class SpringbootWebConfigApplication{
    }
    
    

    但是使用繁琐,性能低

  • 方案二:@Import 导入,使用@Import导入的类会被Spring加载到IOC容器中,导入形式有以下几种

    1. 导入 普通类
    2. 导入 配置类
    3. 导入ImportSelector 接口实现类
    4. @EnableXxxx注解,封装@Import注解(springboot所采用的方式)
    //普通类
    @Import({TokenParser.class})//第三方类
    @SpringBootApplication
    public class SpringbootWebConfigApplication{
    }
    
    
    //配置类
    @Import({HeaderConfig.class})//第三方配置类
    @SpringBootApplication
    public class SpringbootWebConfigApplication{
    }
    
    
    //导入ImportSelector接口实现类
    @Import({MyImportSelector.class})
    @SpringBootApplication
    public class SpringbootWebConfigApplication{
    }
    
    //实现类定义
    public class MyImportSelector implements ImportSelector{
        public String[] selectImports(AnnotationMetadata importingClassMetadata){//类中需要实现的方法
            return new String[] {"com.example.HeaderComfig"};//返回包含需要导入的类名的字符串数组
        }
    }
    
    
    //第三方自定义注解,一般第三方都会配置好,在@Import注解里声明需要导入的类
    @Retention(RetenrionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Import(MyimportSelector.class)//需要导入的类
    public @interface EnableHeaderConfig{
    }
    
    //加上自定义注解
    @EnableHeaderConfig
    @SpringBootApplication
    public class SpringbootWebConfigApplication{
    }
    
    
  • @SpirngBootApplication 注解标识在SpringBoot工程的引导类上,是SpringBoot中最重要的注解,由三部分组成

  • @SpringBootConfiguration:该注解与@Configuration注解作用相同,声明当前类是一个配置类
  • @ComponentScan:组件扫描,默认扫描当前引导类所在包以及子包
  • @EnableAutoConfiguration:SpringBoot实现自动化配置的核心注解

image-20250123001626232

  • @Conditional 条件装配注解

    • 作用:按照一定条件判断,在满足条件之后才会注册对应的bean对象到Spring的IOC容器中
    • 位置:方法、类
    • @Conditional本事是一个父注解,派生处大量的子注解:
      • @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器中
      • @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器中
      • @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册到IOC容器中

十二、Maven高级

(一)分模块设计与开发

将项目按照功能拆分成若干子模块,方便项目的管理维护、拓展,也方便模块之间的相互调用,资源共享

image-20250123143516400

步骤:

  1. 创建maven模块 tilas-pojo,存放实体类

  2. 创建maven模块 tilas-utils,存放相关工具类

    导入对应依赖至新建模块中,刷新依赖

    再在原来程序的xml文件中把新建的模块导入依赖,刷新依赖

    <!--tlias-pojo包-->
            <dependency>
                <groupId>com.itheima</groupId>
                <artifactId>tlias-pojo</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
    

注意:分模块开发需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕再拆分

(二)继承与聚合

继承与聚合

  • 作用
    • 聚合用于快速构建项目
    • 继承用于简化依赖配置、统一管理依赖
  • 相同点
    • 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中
    • 聚合与继承均属于设计型模块,并无实际的模块内容
  • 不同点
    • 聚合是在聚合工程中配置关系,聚合可以感知到参与聚合的模块有哪些
    • 继承时在子模块中配置关系,父模块无法感知哪些子模块继承自己
继承
1.继承关系

概念:继承描述的是两个工程之间的关系,与java中的继承类似,子工程可以继承父工程中的配置信息(依赖),常见于关系的继承,把子工程公共的依赖配置在父工程下

实现:

<parent>
  ......    <!--父工程的坐标-->
</parent>

  1. 创建maven模块tlias-parent,设置打包方式pom(默认jar),同时让其继承spring-boot-starter-parent工程(与java相同单继承机制,此处是因为tlias-web-management模块继承了spring-boot-starter-parent)

        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.7.5</version>
        </parent>
    
        <groupId>com.itheima</groupId>
        <artifactId>tlias-parent</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>pom</packaging>  <!--修改打包方式,用packing-->
    
    
  2. 在子工程的pom.xml文件中配置继承关系

    <parent>
            <groupId>com.itheima</groupId>
            <artifactId>tlias-parent</artifactId>
            <version>1.0-SNAPSHOT</version>
            <relativePath>../tlias-parent/pom.xml</relativePath><!-- ../表示退一级目录  -->
        </parent>
    
    
  3. 在父工程中配置子工程共有依赖

注意事项:

  1. relativePath指定父工程的pom文件的相对位置(如果不指定,将从本地仓库/远程仓库查找该工程)

  2. 如果父子工程都配置了同一个依赖的不同版本,以子工程的为准

  3. 结构说明,实例案例是拆分项目,实际开发可能是第二种

    image-20250123230554046

补充说明:

  • jar:普通的模块打包,springboot项目基本都是jar包(内嵌tomcat运行)
  • war:普通web程序打包,需要部署在外部的tomcat服务器中运行
  • pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理
2.版本锁定

前提条件:不是所有模块有相同的依赖,部分依赖相同时,通过版本锁定来控制其版本号相同(注意:管理并不是导入,管理之后还需要在子工程中导入相关依赖,只是不需要添加版本号,需要单独列出)

在maven中,可以在父工程的pom文件中通过来统一管理版本依赖

此时,子工程引入依赖时,无需指定版本号,父工程统一管理。变更依赖版本只需要在父工程中统一修改

父工程:

<dependencyManagement>
	<dependencies>
		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
	</dependencies>
</dependencyManagement>

子工程:

<dependencies>
		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
</dependencies>

自定义属性/引用属性

image-20250123233143770

   <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        
        <!--自定义属性-->
        <lombok.version>1.18.24</lombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>     <!--${}替代-->
        </dependency>
    </dependencies>

聚合

打包:maven生命周期中的packge

(若本地仓库没有相应依赖模块,需要在maven生命周期中选择install安装到本地仓库)

传统打包方式:打包某个工程时,需要先安装其所依赖的工程以及其父工程,再对项目进行打包,非常繁琐

  • 聚合:将多个模块组织成一个整体,同时进行项目的构建
  • 聚合工程:一个不具有业务功能的“空”工程(有且仅有一个pom文件),可以使用父工程
  • 作用:快速构建项目,无需手动构建

maven中可以通过设置当前聚合工程所包含的子模块名称

聚合工程中所包含的模块,在构建时,会自动根据模块间的依赖关系设置构建顺序,与modules中的顺序无关

<!--聚合-->
    <modules>
        <module>../tlias-pojo</module>            ../表示退一层级(这里表示parent的xml,需要退一层找parent同级的模块)
        <module>../tlias-utils</module>
        <module>../tlias-web-management</module>
    </modules>

(三)私服

介绍:私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,用于代理位于外部的中央仓库,用于解决团队内部的资源共享和资源同步问题

image-20250201160432711

依赖的查找顺序:本地仓库 -》私服 -》中央仓库

一般开发中私服不需要我们自己搭建

image-20250201172104351

 <groupId>com.itheima</groupId>
    <artifactId>tlias-parent</artifactId>
    <version>1.0-SNAPSHOT</version>   //这里就是项目的版本
    <packaging>pom</packaging> 

私服配置说明

访问私服:http://192.168.150.101:8081

访问密码:admin/admin

使用私服,需要在maven的settings.xml配置文件中,做如下配置:

1.需要在 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>

2.在 mirrors 中只配置我们自己私服的连接地址(如果之前配置过阿里云,需要直接替换掉)

<mirror>
    <id>maven-public</id>
    <mirrorOf>*</mirrorOf>
    <url>http://192.168.150.101:8081/repository/maven-public/</url>
</mirror>

3.需要在 profiles 中,增加如下配置,来指定snapshot快照版本的依赖,依然允许使用

<profile>
    <id>allow-snapshots</id>
        <activation>
        	<activeByDefault>true</activeByDefault>
        </activation>
    <repositories>
        <repository>
            <id>maven-public</id>
            <url>http://192.168.150.101:8081/repository/maven-public/</url>
            <releases>
            	<enabled>true</enabled>
            </releases>
            <snapshots>
            	<enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
</profile>

4.如果需要上传自己的项目到私服上,需要在项目的pom.xml文件中,增加如下配置,来配置项目发布的地址(也就是私服的地址)

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

发布项目,直接运行 deploy 生命周期即可 (发布时,建议跳过单元测试)

启动本地私服
解压: apache-maven-nexus.zip

进入目录: apache-maven-nexus\nexus-3.39.0-01\bin

启动服务:双击 start.bat

访问服务:localhost:8081

私服配置说明:将上述配置私服信息的 192.168.150.101 改为 localhost

说真的,这两年看着身边一个个搞Java、C++、前端、数据、架构的开始卷大模型,挺唏嘘的。大家最开始都是写接口、搞Spring Boot、连数据库、配Redis,稳稳当当过日子。

结果GPT、DeepSeek火了之后,整条线上的人都开始有点慌了,大家都在想:“我是不是要学大模型,不然这饭碗还能保多久?”

先给出最直接的答案:一定要把现有的技术和大模型结合起来,而不是抛弃你们现有技术!掌握AI能力的Java工程师比纯Java岗要吃香的多。

即使现在裁员、降薪、团队解散的比比皆是……但后续的趋势一定是AI应用落地!大模型方向才是实现职业升级、提升薪资待遇的绝佳机遇!

如何学习AGI大模型?

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

2025最新版优快云大礼包:《AGI大模型学习资源包》免费分享**

一、2025最新大模型学习路线

一个明确的学习路线可以帮助新人了解从哪里开始,按照什么顺序学习,以及需要掌握哪些知识点。大模型领域涉及的知识点非常广泛,没有明确的学习路线可能会导致新人感到迷茫,不知道应该专注于哪些内容。

我们把学习路线分成L1到L4四个阶段,一步步带你从入门到进阶,从理论到实战。

L1级别:AI大模型时代的华丽登场

L1阶段:我们会去了解大模型的基础知识,以及大模型在各个行业的应用和分析;学习理解大模型的核心原理,关键技术,以及大模型应用场景;通过理论原理结合多个项目实战,从提示工程基础到提示工程进阶,掌握Prompt提示工程。

L2级别:AI大模型RAG应用开发工程

L2阶段是我们的AI大模型RAG应用开发工程,我们会去学习RAG检索增强生成:包括Naive RAG、Advanced-RAG以及RAG性能评估,还有GraphRAG在内的多个RAG热门项目的分析。

L3级别:大模型Agent应用架构进阶实践

L3阶段:大模型Agent应用架构进阶实现,我们会去学习LangChain、 LIamaIndex框架,也会学习到AutoGPT、 MetaGPT等多Agent系统,打造我们自己的Agent智能体;同时还可以学习到包括Coze、Dify在内的可视化工具的使用。

L4级别:大模型微调与私有化部署

L4阶段:大模型的微调和私有化部署,我们会更加深入的探讨Transformer架构,学习大模型的微调技术,利用DeepSpeed、Lamam Factory等工具快速进行模型微调;并通过Ollama、vLLM等推理部署框架,实现模型的快速部署。

整个大模型学习路线L1主要是对大模型的理论基础、生态以及提示词他的一个学习掌握;而L3 L4更多的是通过项目实战来掌握大模型的应用开发,针对以上大模型的学习路线我们也整理了对应的学习视频教程,和配套的学习资料。

二、大模型经典PDF书籍

书籍和学习文档资料是学习大模型过程中必不可少的,我们精选了一系列深入探讨大模型技术的书籍和学习文档,它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础(书籍含电子版PDF)

三、大模型视频教程

对于很多自学或者没有基础的同学来说,书籍这些纯文字类的学习教材会觉得比较晦涩难以理解,因此,我们提供了丰富的大模型视频教程,以动态、形象的方式展示技术概念,帮助你更快、更轻松地掌握核心知识

四、大模型项目实战

学以致用 ,当你的理论知识积累到一定程度,就需要通过项目实战,在实际操作中检验和巩固你所学到的知识,同时为你找工作和职业发展打下坚实的基础。

五、大模型面试题

面试不仅是技术的较量,更需要充分的准备。

在你已经掌握了大模型技术之后,就需要开始准备面试,我们将提供精心整理的大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余。


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

2025最新版优快云大礼包:《AGI大模型学习资源包》免费分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值