苍穹外卖项目练习总结

做这个练习项目已经接近两年之久,现在拿出来复习一遍,主要就是里面处理问题的流程,以及整体思考的逻辑需要重新回顾一遍,后续会逐渐总结这一段时间以来学习到的知识。

项目整体包含两部分,一个是管理端,一个是用户端,管理端是一个网页,用户端是微信小程序的形式,技术栈用的比较全,主要练习增删改查以及redis的基础使用,了解Nginx的一些配置,并且了解小程序和web网页搭配后端的开发练习。本次,先总结前半部分。

目录

一、功能架构和技术栈

二、准备知识

[2.1 VO与DTO:](#2.1 VO与DTO:)

[2.2 Nginx的使用](#2.2 Nginx的使用)

反向代理配置方式:

负载均衡配置:

2.3密码加密

2.4测试工具Swagger

三、员工管理实现

3.1员工登录

3.2新增员工存在的问题

3.3条件分页查询员工

[3.4 启用禁用员工账号](#3.4 启用禁用员工账号)

3.5编辑员工操作

四、菜品管理实现

4.1公共字段填充(重点)

应用AOP+反射+自定义注解实现


一、功能架构和技术栈

二、准备知识

2.1 VO与DTO:

2.2 Nginx的使用

前端请求地址与后端接口地址都不同,前端发送的请求如何到后端的?

使用Nginx反向代理,相当于一个墙挡在各个服务器的前面,浏览器发送请求先到达nginx,然后再由nginx发送到tomcat服务器,为什么浏览器不直接发送到tomcat呢?这是因为采用Nginx有很多优点,可以进行负载均衡,由Nginx均分到各个服务器;还可以保证后端服务的安全性,因为前端无法直接访问到。

反向代理配置方式:

负载均衡配置:

2.3密码加密

为了安全,一般存储到数据库中的密码需要加密,此处采用md5加密,是单向的过程,只能将一个明文加密成密文,无法从密文解密成明文。使用spring自带的工具类完成,后面代码中演示。

2.4测试工具Swagger

使用Swagger测试工具,原先使用的postman,但是参数过多,需要生成多个参数,测试效率就有点低。

使用knife4j对swagger进行封装,简化了操作,需要导入依赖:

配置相应信息:

java 复制代码
 /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public Docket docket1() {
        log.info("正在生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包,扫描之后会对所有的类进行扫描,之后使用反射解析类以及里面的方法,来生成接口文档。
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
    @Bean
    public Docket docket2() {
        log.info("正在生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包,扫描之后会对所有的类进行扫描,之后使用反射解析类以及里面的方法,来生成接口文档。
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    /**
     * 设置静态资源映射(进行静态资源映射之后才能访问到上方生成的接口文档)
     * @param registry
     * 此处的这个方法名不能乱写,是重写的父类里面的方法。
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

三、员工管理实现

3.1员工登录

管理员登录部分中需要注意的地方:

1.使用md5加密,可以使用spring提供的工具类:DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())

2.生成jwt令牌,指定签名的时候使用的签名算法,也就是header那部分,设置加密密钥、名称和过期时间,body中的私有声明claim部分可以去自己设置,此处设置成employeeId,后面将jwt令牌封装到了一个VO中,返回给前端,保存到请求头中,后面从请求头中获取token,然后后续拦截器进行jwt令牌校验根据加密密钥看是否可以解析成功。

3.使用@Builder注解,是Lombok库提供的一个功能,用于简化对象的创建过程,尤其是在需要设置多个属性时。@Builder注解会在编译时自动生成一个内部静态的Builder类,该类包含了所有字段的链式setter方法和一个build()方法用于创建对象实例。

Controller层:

java 复制代码
@ApiOperation(value="员工登录")//添加说明
    @PostMapping("/login")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
        log.info("员工登录:{}", employeeLoginDTO);

        Employee employee = employeeService.login(employeeLoginDTO);

        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(employee.getId())
                .userName(employee.getUsername())
                .name(employee.getName())
                .token(token)
                .build();

        return Result.success(employeeLoginVO);
    }

service层:

java 复制代码
 public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);

        //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
        if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }

        //密码比对
        // 前端传来的明文密码进行md5加密处理:
        password= DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }

        //3、返回实体对象
        return employee;
    }

jwt生成:此处设置了私有声明为employeeId,后续可以通过获取jwt令牌的名称然后获取到这个用户id。

java 复制代码
public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

在yml文件中配置jwt的配置信息,密钥、前端传递过来的令牌名称,过期时间等。然后使用@ConfigurationProperties注解映射到配置类中。

java 复制代码
sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: nihao
    user-token-name: authentication
    user-ttl: 72000000
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}
java 复制代码
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;

}

3.2新增员工存在的问题

第一个需要完善的地方:

新增用户的时候如果用户名重复,没有处理这个异常,可以用全局异常处理器进行处理。复制这个异常,然后在全局异常处理器中单独处理。

复制代码
/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }
    //处理username重复的异常
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'xiaoyan' for key 'idx_username'
        String message=ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg=username+MessageConstant.ALREADY_EXIST;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

}

第二个需要完善的地方:

新增员工的时候在service中需要指定哪个员工新增了员工,需要获取创建者的id,因此需要思考如何获取创建者的id。

可以想到之前结合jwt令牌来实现,将empId存到jwt令牌中,返回给前端,每次可以通过从请求头中来获取jwt令牌,然后解析得到empId即可,但是需要注意的是如何能够保证在本次请求中拿到empId,可以使用ThreadLocal:

ThreadLocal 不是一个线程,而是线程的局部变量,ThreadLocal 可以为每个线程提供一份单独的空间,具有线程隔离效果,只有在线程内才可以获取到对应的值,线程外则不可以访问。

ThreadLocal一般都会进行封装,封装为一个类,包含使用的方法:

复制代码
package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

**实现方式:**在用户登陆的时候将用户id存到jwt令牌中的claim中,然后当后面发起新增用户请求的时候,会经过拦截器,在服务端中拦截器会从请求头中解析出jwt令牌,从而获取empId,然后保存到该线程的ThreadLocal中,后面到达service层的时候可以从ThreadLocal中获取到id,然后设置创建人属性为该empId。

复制代码
/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            //获取用户的id:
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            //存储到ThreadLocal中:
            BaseContext.setCurrentId(userId);
            log.info("当前用户id:", userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

3.3条件分页查询员工

此处使用的是mybatis,并不是plus,使用plus会方便很多(但是容易遗忘sql /(ㄒoㄒ)/~~)

按照姓名条件模糊查询,如果没有条件则默认查询全部,有条件按照条件来查询:

具体实现过程:

前端传过来有三个元素,用户姓名(条件),页数,每页数据量,将其封装为一个DTO接收。返回的是该页的数据以及总数,也封装为一个VO接收,使用PageHelper分页插件来实现,在service中使用PageHelper.startPage(),然后再封装到page中,之后用page.getTotal方法查询方法获取到的总数以及记录。最后封装到PageResult中。

代码如下:

复制代码
//Controller
 @ApiOperation("员工条件分页查询")
    @GetMapping("/page")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){//这里不用加@RequestBody了,前端是Query格式,不是json格式
        log.info("分页查询,参数为"+employeePageQueryDTO);
        PageResult pageResult=employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);

    }
//Service
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
        Page<Employee>page= employeeMapper.pageQuery(employeePageQueryDTO);
        long total = page.getTotal();
        List<Employee> records = page.getResult();

        return new PageResult(total,records);
    }
//Dao
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
//xml映射文件
<select id="pageQuery" resultType="com.sky.entity.Employee">
    select * from employee
    <where>
        <if test="name!=null and name!=''">
            and name like concat('%',#{name},'%')
        </if>
    </where>
    </select>

**注意:**经过测试后发现有些问题, 更新时间和创建时间返回到前端是以数组的形式,前端展示的也会有问题,所以需要修改一下:

有两种解决方案:

1.在属性上添加注解但是这种方式只能针对一个个属性进行转换,所以不推荐。

2.在WebMvcConfiguration配置类(继承WebMvcConfigurationSupport类)中扩展SpringMvc消息转换器,统一对日期格式进行处理:

复制代码
/**
     * 扩展SpringMVC框架的消息转换器,对后端传递给前端的数据进行统一的管理,
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter=new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());//这个JacksonObjectMapper类在common中的json包中,可以看到里面定义好了序列化方法和反序列化方法,将LocalDateTime类型的转化为规定的格式。
        //此时定义好了一个消息转换器但是并没有交给框架,所以框架也不会去使用这个转换器,需要再将这个转换器交给框架,也就是交给converters这个容器中:
        converters.add(0,converter);//注意容器中的消息转换器是有顺序的,加进去之后会默认排到最后,是没法用的,所以前面加个0代表索引。

    }

3.4 启用禁用员工账号

去改变员工账号的状态,如果状态为禁用,则无法登陆系统。这本质上是一个更新操作,但是单独为了这个操作写一个单独更新员工帐号状态属性的方法非常冗余,所以直接写成条件动态sql,根据传入的参数来对相应的属性进行更新,同时登陆操作也需要进行更改,需要增加一步判断,帐号状态是否可用:

接口设计如下:前端传两个参数,一个是员工id,一个是禁用或者启用

整体代码如下:

复制代码
//登录接口修改:添加判断操作
 public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

        //1、根据用户名查询数据库中的数据
        Employee employee = employeeMapper.getByUsername(username);

        //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
        if (employee == null) {
            //账号不存在
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }

        //密码比对
        // 前端传来的明文密码进行md5加密处理:
        password= DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }

        //3、返回实体对象
        return employee;
    }
//Controller:
/**
     * 启用和禁用员工账号
     * @param status
     * @param id
     * @return
     */
    @ApiOperation("启用和禁用员工账号")
    @PostMapping("/status/{status}")
    public Result startAndStop(@PathVariable Integer status,Long id){
        log.info("启用和禁用员工账号,员工账号为:"+status+", "+id);
        employeeService.startAndStop(status,id);
        return Result.success();

    }
//Service:
//启用和禁用员工账号:可以发现这个其实本质上是一个更新操作,但是只更新这两个的话,复用性太差了,所以可以写一个动态sql更新,这样复用性更高。
    @Override
    public void startAndStop(Integer status, Long id) {
        //需要写动态sql,所以要构建一个对象,封装这两个值:
//        Employee employee=new Employee();
//        employee.setStatus(status);
//        employee.setId(id);
        //上面那种构建方法是传统的方法,比较复杂,可以看到Employee实体类的前面添加了Builder注解,可以用这种新的方法:
        Employee employee = Employee.builder()
                .status(status)
                .id(id).
                build();
        employeeMapper.update(employee);
    }
//Mapper动态sql:
    <update id="update">
    update employee
    <set>
        <if test="name!=null">
            name=#{name},
        </if>
        <if test="username!=null">
            username=#{username},
        </if>
        <if test="password!=null">
            password=#{password},
        </if>
        <if test="phone!=null">
            phone=#{phone},
        </if>
        <if test="sex!=null">
            sex=#{sex},
        </if>
        <if test="idNumber!=null">
            id_Number=#{idNumber},
        </if>
        <if test="updateTime!=null">
            update_Time=#{updateTime},
        </if>
        <if test="updateUser!=null">
            update_user=#{updateUser},
        </if>
        <if test="status!=null">
            status=#{status},
        </if>
    </set>
        where id=#{id}
    </update>

3.5编辑员工操作

这个地方需要注意的就是,点击编辑按钮后,需要先展示出来对应的员工信息,所以需要先进性查询回显操作,然后再进行更新操作,更新复用上面写的启用禁用员工账号的动态更新sql。其他没有特别需要注意的地方,整体代码如下:

复制代码
//Controller
/**
     * 根据id查询员工
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询员工")
    public Result<Employee> getById(@PathVariable Long id){
        Employee employee;
        employee=employeeService.getById(id);
        return Result.success(employee);
    }
    /**
     * 编辑员工信息
     * @param employeeDTO
     * @return
     */
    @PutMapping
    @ApiOperation("编辑员工操作")
    public Result update(@RequestBody EmployeeDTO employeeDTO){//此处传过来的属性值,还是和这个属性对应,所以依然可以用这个DTO
        log.info("编辑员工操作"+employeeDTO);
        employeeService.update(employeeDTO);
        return Result.success();

    }
//Service
//编辑员工操作------先根据id查询:
    public Employee getById(Long id) {
        return employeeMapper.getById(id);
    }
    //编辑员工操作
    public void update(EmployeeDTO employeeDTO) {
        Employee employee=new Employee();
        BeanUtils.copyProperties(employeeDTO,employee);
//        employee.setUpdateUser(BaseContext.getCurrentId());
//        employee.setUpdateTime(LocalDateTime.now());
        employeeMapper.update(employee);
    }
}
//DAO
@AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);
    //编辑员工操作------先根绝id查询
    @Select("select *from employee where id=#{id}")
    Employee getById(Long id);

以上是员工管理部分,下面是菜品管理,其中最重要的就是AOP的使用,使用aop实现公共字段填充,可以当作aop以及自定义注解的练习,公共字段填充目前可以直接使用mybatis-plus来完成,简化了很多操作,底层也是aop实现的,无需手动去实现。

四、菜品管理实现

4.1公共字段填充(重点)

应用AOP+反射+自定义注解实现

问题分析:业务表中基本都存在着创建时间、更新时间、创建人、修改人等公共字段,在员工管理以及分类管理中存在许多这样的字段,每当增和改操作的时候都需要在Service层中手动赋值或修改这些字段,重复性很高,非常冗余,所以可以使用一个统一的方法去解决,这就很容易联想到AOP的使用:

只有insert和update操作需要对公共字段修改,所以要提取出这些操作,然后使用AOP对这些进行统一的赋值。

步骤如下:

1.自定义注解AutoFill,用于标识进行公共字段填充的方法

2.自定义切面类,AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。

3.在Mapper方法上加入AutoFill注解

首先自定义注解AutoFill,然后在注解中指定数据库操作的类型,此处使用枚举实现:

(@Retention(RetentionPolicy.RUNTIME)是Java注解中的一个元注解,用于指定被注解的元素在什么时候有效。具体来说,它定义了被注解的元素的保留策略,即注解在运行时仍然有效。)

复制代码
/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target({ElementType.METHOD})//表示添加的目标,此处指定加在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //指定数据库操作类型:UPDATE、INSERT 更新和插入操作;在common模块中的enumeration包中存在这个OperationType枚举;
    OperationType value();
}

枚举类:

复制代码
/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}

然后自定义一个切面类:在里面完善好切入点和通知:

切入点:

复制代码
/**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..))&& @annotation(com.sky.annotation.AutoFill)")//注意此处需要指定用于哪些方法,
    // 不仅要满足execution指定的包下的方法还得满足有这个注解才可以,因为只有execution内的指的是所有的方法,此处只需要对insert和update方法
    public void autoFillPointCut(){}

通知:首先应该使用前置通知,并指定需要拦截的方法,明确操作数据库的方法类型是insert还是update,确定需要增强的方法获取该方法的参数对象,便于为后面填充赋值,准备好赋值数据,之后通过反射来得到set方法对其赋值。

(其中getSignature():这个方法返回当前连接点的签名信息,即Signature对象。签名对象包含了方法或构造函数的基本信息,比如名称、参数等。)

复制代码
@Before("autoFillPointCut()")//指定哪个方法
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始进行公共字段自动填充...");
        //开始对自动填充进行完善:
        //首先要获取到是insert方法还是update方法,如果是update方法只需要对update_user/update_time公共字段进行填充,
        // 如果是insert方法需要对四个公共字段都要进行填充,因此要获取到当前被拦截方法上的数据库操作类型:是insert还是update
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//(方法签名对象)本来是Signature类型,因为拦截到的是一个方法所以进行向下转型,变为MethodSignature
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        OperationType operationType = autoFill.value();//获取数据库操作类型
        //获取到当前被拦截的方法的参数--实体对象(为这个对象的属性(公共参数)赋值)
        Object[]args=joinPoint.getArgs();//获取到所有的参数:args,此处有一个约定就是将对象放到insert方法或者update方法的第一个参数上
        if(args==null||args.length==0) {
            return;
        }
        Object entity=args[0];
        //准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();
        //根据获取到的不同操作类型,为对应的属性通过反射进行赋值
        if(operationType==OperationType.INSERT){
            //为四个公共字段进行赋值
            //获取set方法:
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDateTime.class);//第一个参数是方法名,第二个参数是传入值的类型
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
                setCreateUser.invoke(entity,currentId);
                setCreateTime.invoke(entity,now);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }else if(operationType==OperationType.UPDATE){
            //为两个公共字段进行赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }





    }

整体流程就是:首先自定义注解,然后表明包含的数据库操作类型方法(Insert和Update),然后自定义切面类,表明切入点(在响应包下并且带有AutoFill注解的方法才会被增强),接下来通过前置通知,首先 获取Signature签名对象,然后通过反射得到方法上面的注解,并获取其中的value也就是数据库操作的方法类型,根据方法类型接下来通过反射获取对应的set方法,为相应的公共字段赋值即可完成。

相关推荐
AI人工智能+电脑小能手2 分钟前
大白话说Java设计模式-23-桥接模式(源码剖析篇)
java·设计模式·jdbc·桥接模式·源码分析·awt·java logging
李高钢9 分钟前
C# WPF Prism 进阶(二):区域(Region)与模块化(Module)
java·前端·数据库
long31611 分钟前
枚举(Enums)
java·开发语言·数据库
墨雨晨曦8831 分钟前
2026/08/15 spring AI学习总结
java·tomcat
fatcoder43 分钟前
玩转Nginx 03 — location 匹配规则:让不同的路径各回各家
前端·后端·nginx
wno7041 小时前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
wear工程师1 小时前
Spring Boot 服务挂了却没重启?先分清 systemd 的三次判断
spring boot
聊浮游1 小时前
JAVA2026最新全套学习资料、学习路线
java·开发语言·jvm·mysql·spring·maven·idea
MacroZheng1 小时前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
月华路2 小时前
Guava 图数据结构 graph 从基础概念到原理分析
java·数据结构·guava·图搜索