苍穹外卖学习记录(一)

1.JWT令牌认证

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

在这里插入图片描述

JWT是目前最常用的一种令牌规范,它最常用于保存用户的登录信息。

在这里插入图片描述
JWT与Session的差异 相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

那么我们是如何实现的呢,我们只看他的后端代码:

public class TestUse {
    public static void main(String[] args) {
        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        String jws = Jwts.builder().setSubject("NB").signWith(key).compact();
        System.out.println(jws);
    }
}
  1. 根据指定的签名算法,安全随机生成一个SecretKey
  2. 建立一个JWT,它将 sub(主题)设置为NB,NB是密钥,不能泄露。
  3. 使用适用于HMAC-SHA-256算法的密钥对JWT进行签名。
  4. 最后compact将其压缩成最终String形式。签名的JWT称为“ JWS”。

我们一般都会对其进行封装,下面是调用代码:

		Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

封装的创建JWT令牌方法:

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

2.Ngnix服务器

在这里插入图片描述

如何实现负载均衡?

在这里插入图片描述

下面是我们在nginx中的配置:我们可以看到,负载均衡用的也是proxy_pass,即也是基于反向代理实现的

在这里插入图片描述

在这里插入图片描述

3.VSCode编译器

这个软件是用于开发前端代码的,但其也支持Python、Java的扩展,功能十分强大。

在这里插入图片描述
在这里插入图片描述

4.MD5明文加密

我们看到,数据库中的密码是以明文形式存储的,这是十分危险的,因此我们需要对其进行加密。

在这里插入图片描述我们使用的加密算法便是MD5加密方式。

MD5(Message Digest Algorithmn)是一种广泛使用的密码散列函数,用于生成一个 128
位的散列值,以确保信息传输的完整性和一致性1。虽然它理论上是一种不可逆的加密算法,但我们可以使用在线工具来进行 MD5 加密和解密。

它可以将一段字符串转换为32位的加密字符,注意,这个过程理论上是不可逆的,即只能将字符串转换为加密字符而不能将加密字符转换回来,那么,我们的登录逻辑事实上就是将我们输入的密码也进行MD5加密生成密文,在与数据库中的密文进行比较,如果相同,则认为登录成功,否则登录失败。

那么,在我们的代码中该如何实现呢,具体需要修改两个地方:
第一个就是存储密码时(注册时)对密码进行加密并插入数据库;
另一个则是登录时对密码进行MD5加密,将加密后的字符与数据库中的密文进行比较。

首先是新增用户流程:
Controller层,这里使用了RESTful风格,同时传入转换为JSON格式的员工对象employeeDTO

 @PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO) {
        log.info("新增员工:{}",employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }

随后在Service(业务层)的代码如下:

public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();

        //对象属性拷贝
        BeanUtils.copyProperties(employeeDTO, employee);

        //设置账号的状态,默认正常状态 1表示正常 0表示锁定
        employee.setStatus(StatusConstant.ENABLE);

        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

//        通过ThreadLocal获取用户信息
        Long currentId = BaseContext.getCurrentId();

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(currentId);//目前写个假数据,后期修改
        employee.setUpdateUser(currentId);

        employeeMapper.insert(employee);//后续步骤定义
    }

由于添加用户的界面中没有设置密码框,因此使用默认密码employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
随后便是数据持久化层将数据添加到数据库中了。

<insert id="insert">
        insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) values (#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
</insert>

以上是添加员工的步骤,那么登录该如何做呢?Controller做了三个工作,分别是查询员工信息,生成JWT令牌以及返还员工信息。我们只关注查询用户信息即可,调用的是service层的login方法

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

login方法的实现:这里将一些情况做了异常处理,即直接抛出异常即可。同时,Spring中为我们提供了MD5的加密类,我们调用其内的加密方法即可。

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

        //密码比对
        // TODO 后期需要进行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;
    }

5.接口文档管理工具

这个接口文档是前后端人员经过多次讨论与研究确定下来的,同时这个接口在开发过程中可能也会发现一些问题,因此在开发过程中也会进行修改。
这里我们可以看到资料中有两个接口文档,都是JSON格式的,那么我们该如何查看呢,我们使用的是YAPI这个网站工具。

5.1 YApi接口管理工具

YApi是通过项目的方式来进行接口管理的,因此 我们需要先创建一个项目,随后我们点击数据管理,选择导入的数据格式。

在这里插入图片描述
随后我们点击接口,就可以看到我们的接口了。这是方便我们查看的工具。

在这里插入图片描述

5.2 Apifox接口管理工具

同时,我们在这里推荐一个功能更为强大的接口管理工具Apifox。
(它提供了网站与软件两种形式,我们使用网站版即可)
可以看到,这里面集成了Postman、Swagger等工具的功能

在这里插入图片描述
这里我们选择YApi的形式。其支持多种格式。

在这里插入图片描述

我们开发完后端接口后,先前都是通过Postman进行测试的,但是当接口过多时,该如何处理呢,这时候便可以使用Swagger工具了。

6.Swagger接口管理工具

通过Swagger可以生成后端接口文档,同时可以实现在线接口调试。
1.导入knife4jmaven坐标

在这里插入图片描述

2.在配置类中加入 knife4j相关配置

在这里插入图片描述

@Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

3.设置静态资源映射,否则接口文档页面无法访问

在这里插入图片描述

 protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始设置静态资源映射...");
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");//代表发送/doc.html请求时将其映射到classpath:/META-INF/resources/下
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

将上面的内容配置好后,我们将项目重新启动,可以看到,在WebMVCConfiguration下面的这些方法全部被执行了。

在这里插入图片描述

这一切都是因为这个文件被我们加了@Configuration注解的原因,将其当作配置文件加载了。
随后我们访问我们的文档接口地址:localhost:8080/doc.html,我们就可以访问到Swagger为我们动态创建的接口管理页面了。

在这里插入图片描述

通过 Swagger 就可以生成接口文档,那么我们就不需要 Yapi 了?

  1. Yapi是设计阶段使用的工具,管理和维护接口
  2. swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

通过Swagger注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

那么,这个Swagger我们该怎么用呢,只需要打开要测试的接口,然后点击调试即可:

在这里插入图片描述
这时,我们发现并没有期望的响应值,反而出现了状态码401,这是什么原因呢,这是因为我们的拦截器设置需要检验身份令牌所导致的。这个拦截器接口是Spring写好的,我们只需要实现这个即可即可,里面的preHandle方法代表在每次操作前执行。

@Component
@Slf4j
public class JwtTokenAdminInterceptor 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.getAdminTokenName());
        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
//            将用户id存储到ThreadLocal
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

下面给出其实现的三个方法。

@Component
public class ProjectInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.print("preHandle\n");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.print("postHandle\n");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.print("afterCompletion\n");
    }
}

了解了原因后,我们该如何解决呢,只需要设置一个全局参数,里面的内容是JWT令牌即可。
我们首先使用员工登录接口创建一个合法的身份令牌:

在这里插入图片描述

随后将这个令牌设置为全局参数:

在这里插入图片描述

随后再次执行刚刚的功能:

在这里插入图片描述

7.实体类POJO的划分

我们在之前的开发过程中,都会使用到实体类,如用户类,订单类等,我们称这些实体类为POJO,但是随着开发的不断规范,POJO也有了 新的划分 。

在这里插入图片描述

VO(Value Object)值对象

VO就是展示用的数据,不管展示方式是网页,还是客户端,还是APP,只要是这个东西是让人看到的,这就叫VO。VO主要的存在形式就是js里面的对象(也可以简单理解成json)

PO(Persistant Object)持久对象

PO比较好理解,简单说PO就是数据库中的记录,一个PO的数据结构对应着库中表的结构,表中的一条记录就是一个PO对象,通常PO里面除了get,set之外没有别的方法。

BO(Business Object)业务对象

BO就是PO的组合,即多个PO的集合即为BO

更为详尽的解释如下:

  • VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
  • DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
  • DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
  • PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

8.新增员工功能开发

首先,需要设计要接受的员工对象所对应的DTO(Data Transfer Object,数据传输对象),事实上我们也可以使用原始的员工实体类来接收,但这样会有很多数据没有赋值,因此,当前端提交的数据和实体类中的数据差距较大时,建议使用DTO来封装数据。
在这里插入图片描述
Controller层实现添加用户

	@PostMapping//RESTful风格,代表添加
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO) {
        log.info("新增员工:{}",employeeDTO);//这是log,会在下面控制台输出,{}是占位符,会将employeeDTO填充到{}中。
        employeeService.save(employeeDTO);
        return Result.success();
    }

在业务层中,主要完成的任务是将原本Employee类型的对象数据通过对象属性拷贝方法拷贝到Employee对象中,并将Employee没有的值进行设置,最终将数据插入到数据库中。

public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();

        //对象属性拷贝,这是Spring提供的一个方法,由于employeeDTO是我们接收的数据对象,与我们插入到数据库中的实体类还是有差别的,因此 我们使用对象属性拷贝方法将employeeDTO内的属性值拷贝到Employee类型的对象中。
        BeanUtils.copyProperties(employeeDTO, employee);

        //设置账号的状态,默认正常状态 1表示正常 0表示锁定
        employee.setStatus(StatusConstant.ENABLE);

        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

//        通过ThreadLocal获取用户信息
        Long currentId = BaseContext.getCurrentId();

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(currentId);//目前写个假数据,后期修改
        employee.setUpdateUser(currentId);
        //TODO
        employeeMapper.insert(employee);//后续步骤定义
    }

持久层(Mapper操作)

<insert id="insert">
insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) values (#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
</insert>

9.面向切面编程

从上面的内容中我们看到在员工添加功能的业务层中,需要对其余的参数设置属性,如设置添加时间,修改时间这些内容,事实上,在数据库中很多表都有这个字段,那么我们是否可以将这部分功能给封装一下呢,对于这些功能很类似的模块我们就可以使用到Spring的面向切面编程了,同时,也涉及到自定义注解。

10.注解

注解是提供一种为程序元素设置元数据的方法,程序元素就是指接口、类、属性、方法,这些都是属于程序的元素,那啥叫元数据呢?就是描述数据的数据(data about data),举个简单的例子,系统上有一个sm.png文件,这个文件才是我们真正需要的数据本身,而这个文件的属性则可以称之为sm.png的元数据,是用来描述png文件的创建时间、修改时间、分辨率等信息的,这些信息无论是有还是没有都不影响它作为图片的性质,都可以使用图片软件打开。

注解的分类

​ 通常来说注解分为以下三类

  1. 元注解 – java内置的注解,标明该注解的使用范围、生命周期等。
  2. 标准注解 – Java提供的基础注解,标明过期的元素/标明是复写父类方法的方法/标明抑制警告。标准注解有一下三个:@Override 标记一个方法是覆写父类方法,@Deprecated 标记一个元素为已过期,避免使用
  3. 自定义注解 – 第三方定义的注解,含义和功能由第三方来定义和实现。

元注解

用于定义注解的注解,通常用于注解的定义上,标明该注解的使用范围、生效范围等。元XX 都代表最基本最原始的东西,因此,元注解就是最基本不可分解的注解,我们不能去改变它只能使用它来定义自定义的注解。元注解包含以下五种: @Retention、@Target、@Documented、@Inherited、@Repeatable,其中最常用的是@Retention@Target下面分别介绍一下这五种元注解。

首先是@Retention注解,用于设置生命周期。我们一般都设置为RUNTIME

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

从编写Java代码到运行主要周期为源文件→ Class文件 → 运行时数据@Retention则标注了自定义注解的信息要保留到哪个阶段,分别对应的value取值为SOURCE →CLASS→RUNTIME

1. SOURCE 源代码java文件,生成的class文件中就没有该信息了
2. CLASS class文件中会保留注解,但是jvm加载运行时就没有了
3. RUNTIME 运行时,如果想使用反射获取注解信息,则需要使RUNTIME,反射是在运行阶段进行反射的

@Target注解,中文翻译为目标,描述自定义注解的使用范围,允许自定义注解标注在哪些Java元素上(类、方法、属性、局部属性、参数…)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

value是一个数组,可以有多个取值,说明同一个注解可以同时用于标注在不同的元素上。value的取值如下:

在这里插入图片描述
示例:自定义一个注解@MyAnnotation1想要用在类或方法上,就可以如下定义:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAnnotation {
    String description() default "";
}
@MyAnnotation
public class AnnotationTest {
    // @MyAnnotation   用在属性上则会报错
    public String name;
    @MyAnnotation
    public void test(){}

}

至于其他的元注解,则很少用到。

自定义注解

自定义注解的格式如下:

public @interface 注解名 {
  修饰符 返回值 属性名() 默认值;
  修饰符 返回值 属性名() 默认值;
}

其支持的返回值类型有:

1. 基本类型 int float boolean byte double char logn short
2. String
3. Class
4. Enum
5. Annotation(注解)
6. 以上所有类型的数组类型

// 保留至运行时
@Retention(RetentionPolicy.RUNTIME)
// 可以加在方法或者类上
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface RequestMapping {
    public String method() default "GET";
    public String path();
    public boolean required();
}

其实,总结起来,注解就是给方法、类设置一些属性。

11.反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

Classclass是不同的两个点,Class本身也是一个类型,和StringList本身没有什么差异, 而class只是一个关键字。Class可以理解为某个类型的元信息,包含其对应的构造函数(Constructor)、方法(Method)、属性(Field)以及其他相关信息(比如注解等信息),通过反射,也就是操作Class具体的对象,我们可以在运行期获取一个类型中各种访问权限的构造器、方法、属性,灵活的去创建某个类型的实例,调用其方法,设置其属性。可以认为这是Java提供的一个外挂,让我们可以做一些常规操作不能做到的操作。

那么具体该如何获取呢?
只要调用Class的相应方法即可获取,而这些方法的命名是具有共同的特征的。

  1. 获取所有构造器
    获取所有公开的构造器使用getConstructors()。获取所有(包含public/protected/default/private的构造器使用getDeclaredConstructors()
  2. 获取所有的方法
    获取所有公开的方法使用getMethods(),同时会返回父类的所有公开方法。获取所有访问权限的方法使用getDeclaredMethods()
  3. 获取所有的属性
    获取所有公开权限的属性getFields(),同时会返回父类的公开属性。获取所有访问权限的属性.getDeclaredFields()

我们给出一个示例:

package Reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class FelectTest {
    private int id;
    private String name;
    public void show(String name){
        System.out.println(name);
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public FelectTest(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public FelectTest(int id) {
        this.id = id;
    }
    public FelectTest() {
    }
    @Override
    public String toString() {
        return "FelectTest{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException, IllegalAccessException, InstantiationException {
        Class<FelectTest> felect= (Class<FelectTest>) Class.forName("Reflect.FelectTest");
        System.out.println("类对象:"+felect);//类对象
        Constructor[] constructors=felect.getConstructors();
        System.out.println("无参构造方法:"+constructors);//无参构造函数

        Constructor constructorint=felect.getConstructor(int.class);
        System.out.println(constructorint);//形参为int的构造函数
        Object obj=constructorint.newInstance(1);//使用构造函数创建对象
        Method method=felect.getMethod("setName",String.class);//获取名字为setName,参数为String类型的方法
        method.invoke(obj,"李白");//执行方法,需要传入对象和参数
        Method methodget=felect.getMethod("getName");//获取名字为getName的get方法
        methodget.invoke(obj);//执行getName方法

        Field[] fields=felect.getDeclaredFields();//获取所有属性
        for (Field field : fields) {
            System.out.println("属性:"+field);
        }

        Field id=felect.getDeclaredField("id");
        System.out.println(id);//获取属性名称为id的属性
        Method methods=felect.getDeclaredMethod("show",String.class);
        Object object=felect.getConstructor().newInstance();
        methods.invoke(object,"李白");
    }
}

在这里插入图片描述

还记得员工添加功能中设置添加人,修改人,添加时间等属性吗,这在其他表中,如菜品表,类别表中也存在,那么对于这些都具备的功能,我们就可以利用面向切面编程来封装起来,同时我们还要知道当前的方法执行的是什么操作,因为我们在执行修改功能时是不需要设置添加人和添加时间的。另外,由于涉及多个对象,如员工,用户,菜品,因此我们需要获取对应对象的方法来实现。

12.公共字段填充

如何实现公共字段填充呢?需要下面几个步骤:

  1. 自定义注解 AutoFi,用于标识需要进行公共字段自动填充的方法
  2. 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFi 注解的方法,通过反射为公共字段赋值在 Mapper
    的方法上加入 AutoFi 注解
  3. 那么我们该如何实现呢,首先是定义我们要执行的方法类型:

定义枚举:

在这里插入图片描述

在这里插入图片描述

随后定义注解,这个注解用于标识我们执行的是哪种方法:
创建时使用创建注解方式。

在这里插入图片描述

随后通过元注解定义生命周期和所能注解的方法。

在这里插入图片描述

package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 自定义注解,用于标识某个方法需要进行字段自动填充
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//    数据库操作类型
    OperationType value();
}

接着我们将这个注解添加到我们要执行的方法上,一般情况下我们为了解耦合,都是将其放到接口上而非具体的实现方法上。

将注解添加到Mapper接口的方法上:

//修改员工,设置注解操作方式为INSERT
@AutoFill(OperationType.INSERT)
void insert(Employee employee);
//修改员工,设置注解操作方式为UPDATE
@AutoFill(OperationType.UPDATE)
void update(Employee employee);

完成这个注解设置后,便是面向切面编程设置了。

首先定义切入点,这里是匹配Mapper里面的所有方法,同时还要该方法使用AutoFill注解的。

@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
    System.out.println("切入点");
}

随后便是设置切面的具体操作了,设置前置通知,即对公共字段进行填充。
具体,则是使用反射实现的。

/**
     * 前置通知,在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {//JoinPoint即为连接点
        log.info("开始进行公共字段自动填充");

//        获取到当前拦截的方法上的数据库操作类型
//        获取方法签名对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

//        获取方法上的注解对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);

//        获取数据库操作类型
        OperationType operationType = autoFill.value();

//        获取到当前被拦截的方法的参数---实体对象
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) {
            return;
        }

        Object entity = args[0];

//        转变赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

//        根据当前不同的操作类型,为对应的属性通过反射来赋值
        if (operationType == OperationType.INSERT) {
//            为4个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

//              通过反射为对象赋值
                setCreateTime.invoke(entity, now);
                setCreateUser.invoke(entity, currentId);
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        } else if (operationType == operationType.UPDATE) {
//            为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

//              通过反射为对象赋值
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }


    }

在这里插入图片描述

通过反射可以读取该方法所使用的注解,用于确定我们的操作类型:

//        获取方法上的注解对象
AutoFill autoFill =signature.getMethod().getAnnotation(AutoFill.class);
//        获取数据库操作类型
OperationType operationType = autoFill.value();

根据我们刚刚DeBug的结果,我们可以分析出实体对象保存在args[0]中。

//获取到当前被拦截的方法的参数---实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
    return;
 }
Object entity = args[0];

随后便是根据操作类型判断是进行哪种操作了:就是获取对象的方法然后通过invoke来执行这些方法。

		if (operationType == OperationType.INSERT) {
//            为4个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//              通过反射为对象赋值
                setCreateTime.invoke(entity, now);
                setCreateUser.invoke(entity, currentId);
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else if (operationType == operationType.UPDATE) {
//            为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//              通过反射为对象赋值
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

至此,面向切面编程实现公共字段属性填充便实现了。

13.异常处理

在SpringBoot中,对异常的处理方式主要可分为三种:

  1. 自定义全局异常
  2. 手动抛出异常
  3. 测试打印异常

自定义全局异常

SpringBoot中,@ControllerAdvice 即可开启全局异常处理,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(value =Exception.class)
    @ResponseBody
    public String exceptionHandler(Exception e){
        System.out.println("全局异常捕获>>>:"+e);
        return "全局异常捕获,错误原因>>>"+e.getMessage();
    }
}

至于其他的方式,则并不优雅。在这里,既然我们使用了SpringBoot框架,那么自然便使用这种注解方式处理异常最佳,在先前的员工添加功能中,我们还要对这个功能进行完善,即不允许添加员工的账号相同:

如果发生了这种情况,在不进行异常处理时,便会提示:SQLIntegrityConstraintViolationException那么,我们只需要自定义全局异常时将其捕获即可:

package com.sky.handler;

import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){//捕获的异常类型
        //Duplicate entry 'zhangsan' for key 'employee.idx_username'
        String message = ex.getMessage();//提取异常信息,就是上面的这句话
        if(message.contains("Duplicate entry")){//我们想将这个异常信息中的zhangsan提取出来,提示已经存在,因此便将上面的字符串进行分割,并拼接上定义好的常量提示信息:MessageConstant.ALREADY_EXISTS
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

在这里插入图片描述

14.ThreadLocal

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

学习这个有什么用呢?还记得我们在员工添加时需要将添加入的信息保存到数据表中吗,原本我们该如何做呢,我们可以使用Session,但如今我们使用JWT认证来实现用户信息校验,JWT令牌中存储着用户信息,那么我们就可以将这个信息解析出来,但JWT的信息解析是放在我们的拦截器中的,我们而用户信息则是在我们的业务层中使用 ,问题是如何将这个信息传递到业务层中呢?这就用到了我们的ThreadLocal存储空间了,由于整个业务是同一个线程,因此我们就可以将数据保存在里面。

那么,ThreadLocal怎么使用呢?
ThreadLocal常用方法:
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
public void remove() 移除当前线程的线程局部变量

下面是JWT令牌认证的过程:
在这里插入图片描述
那么该如何如何实现呢?首先我们定义一个BaseContext 类,里面生成了一个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();
    }
}

随后我们只需要把JWT令牌解析出来的用户信息添加到ThreadLocal中即可。
JWT定义的常量信息

package com.sky.constant;

public class JwtClaimsConstant {

    public static final String EMP_ID = "empId";
    public static final String USER_ID = "userId";
    public static final String PHONE = "phone";
    public static final String USERNAME = "username";
    public static final String NAME = "name";

}

在登录时将用户信息保留到JWT令牌中。

在这里插入图片描述

JWT令牌中的信息解析出来并添加到ThreadLocal对象中。
在这里插入图片描述

随后通过下面的方法就可以获取出我们保存的数据了。

Long currentId = BaseContext.getCurrentId();

15.分页查询 PageHelper

我们首先看一下接口文档的要求,请求的数据类型是query,即是以字符串拼接到浏览器地址的形式实现的,所需要的参数有三个。

在这里插入图片描述
以前博主开发时,会都是用一个实体类来封装这些数据,这确实不规范,我们此时定义一个专门用于封装接收数据的实体类:

package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmployeePageQueryDTO implements Serializable {
    //员工姓名
    private String name;
    //页码
    private int page;
    //每页显示记录数
    private int pageSize;
}

随后我们定义Controller层方法:

@GetMapping("/page")
    @ApiOperation("员工分页查询")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {//这里就不要@RequestBody注解了,因为是jquery。
        log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }

随后是业务层实现,为了让代码更简洁,使用PageHelper插件。

 @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//        开始分页查询,这是基于select * form table limit 0,10来实现的,但我们可以通过PageHelper插件来实现分页sql拼接功能。
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
//      要想使用PageHelper插件,就要符合其规则,查询的返回结果为Page类型,泛型为Employee
        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
//      获取记录数与结果
        long total = page.getTotal();
        List<Employee> records = page.getResult();
//      封装到PageResult中
        return new PageResult(total, records);
    }

书写对应的SQL语句,由于name属性是动态的,因此要用动态SQL,而注解写的SQL很不方便,因此用mapper.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>

这里我们可能会有疑问,这个PageHelper.startPage方法似乎和后面的查询没关系吧,事实上,PageHelper.startPage是基于ThreadLocal实现的,它会将我们传递的limit的起始和页数保存起来,然后拼接到我们的xml中的SQL语句中。

16.拦截器 Interceptor

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

作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

FilterInterceptor区别

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

拦截器,是用于处理

那么,在SpringBoot中该如何使用拦截器呢?需要以下两个步骤:

1.配置拦截器

配置拦截器只需要实现HandlerInterceptor接口,并重写其所有方法即可。

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override //目标方法执行前的执行,返回true放行,返回false不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }
 
    @Override //目标方法执行后执行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
 
    @Override //视图渲染执行后执行,最后执行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

JWT令牌拦截器配置如下:

package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

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

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);

//            将用户id存储到ThreadLocal
            BaseContext.setCurrentId(empId);

            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

2.注册拦截器

在配置完拦截器后,我们需要在配置类中注册改拦截器,只需要实现WebMvcConfigurer 接口中的方法即可

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//拦截所有请求
    }
}

这个注册拦截器的作用是告诉配置文件我们有拦截器,并且负责告诉系统哪些要拦截哪些不拦截,当然上面是通过实现WebMvcConfigurer 接口的方式完成的,也可以通过继承的方式重写WebMvcConfigurationSupport 里面的方法,比如我们书写的代码中包含JWT令牌验证,资源过滤等功能。

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;
    /**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");

        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")//要拦截的请求
                .excludePathPatterns("/admin/employee/login");
                //不要拦截的请求
        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }
}

17.拓展消息转换器

我们将员工数据查询出来后,发现时间格式不是我们所期望的。如下:

在这里插入图片描述
有两种解决方式,第一种是使用@JsonFormat注解

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

但这种方式需要对每个需要转换的数据都添加一个注解,十分麻烦,因此选择第二种解决方法,使用拓展消息转换器extendMessageConverters
这个配置也是在WebMvcConfiguration中,WebMvcConfiguration是我们自己定义的配置类,它继承了WebMvcConfigurationSupport,我们重写extendMessageConverters方法即可(该方法是固定的)。

protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象(Spring提供的)
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据(这个对象转换器是自己定义的,是一种工具类,会用即可)
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中,converter是一个容器,是所有消息转换器的集合。同时在converter有许多java自定义的消息转换器,我们使用add方法默认是加在最后面的,这会导致难以被使用到,因此加一个索引0
        converters.add(0,converter);
    }

对象转换器的定义如下,其作用如下:

  • 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
  • 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
  • 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
package com.sky.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

在这里插入图片描述

18.@RequestParam与@PathVariable

这两种都是将前端发送的数据传递到Controller,那么有什么不同呢?

@RequestParam传递的是Request参数,当然我们可以不写这个注解,默认就是这种。即?参数名=参数值 形式
@PathVariable传递的是URL变量:在@RequestMapping注解中用{ }来表明它的变量部分,例如:

@RequestMapping(value="/user/{username}")

单个URL变量:

	@RequestMapping(value="/user/{username}")
    public String userProfile(@PathVariable(value="username") String username) {
    	return "user"+username;
    }

当有多个URL变量时:

	@RequestMapping(value = "/user/{username}/blog/{blogId}")
    public String getUserBlog(@PathVariable String username, @PathVariable int blogId) {
    	return "user:" + username + "blog->" + blogId;
    }

当两者共用时:@PathVariable 中参数名相同时可以不写参数名,@RequestParam可以省略

	@PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账户")
    public Result startOrStop(@PathVariable Integer status, Long id) {
        log.info("启用禁用员工账户:{},{}", status, id);
        employeeService.startOrStop(status, id);
        return Result.success();
    }

19.实体类中的注解

在实体类上加入@Builder注解后,我们就可以使用builder(构造器)来创建对象。这个效果与new对象后设置属性效果相同。

Employee employee = Employee.builder()
                .status(status)
                .id(id)
                .build();
  • @Data//省去代码中大量的get()、 set()、 toString()等方法
  • @Builder//允许Builder构造器创建对象注解
  • @NoArgsConstructor//生成无参构造方法注解
  • @AllArgsConstructor//生成所有有参构造方法注解
package com.sky.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data//省去代码中大量的get()、 set()、 toString()等方法
@Builder//允许Builder构造器创建对象注解
@NoArgsConstructor//生成无参构造方法注解
@AllArgsConstructor//生成所有有参构造方法注解
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;

    private Integer status;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

    private Long createUser;

    private Long updateUser;

}

20.动态SQL语句

先前在查询时使用过一次了,那么在修改时我们可能会对多个字段进行修改,因此可以使用动态SQL语句来实现。

动态修改

<update id="update" parameterType="Employee">
        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>

动态查询

<select id="list" resultType="com.sky.entity.ShoppingCart">
        select *
        from shopping_cart
        <where>
            <if test="userId!=null">and user_id=#{userId}</if>
            <if test="dishId!=null">and dish_id=#{dishId}</if>
            <if test="setmealId!=null">and setmeal_id=#{setmealId}</if>
            <if test="dishFlavor!=null">and dish_flavor=#{dishFlavor}</if>
        </where>
        order by create_time desc
    </select>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/551007.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【学习笔记】Python大数据处理与分析——pandas数据分析

一、pandas中的对象 1、Series对象 由两个相互关联的数组(values, index)组成&#xff0c;前者&#xff08;又称主数组&#xff09;存储数据&#xff0c;后者存储values内每个元素对应关联的标签。 import numpy as np import pandas as pds1 pd.Series([1, 3, 5, 7])print(…

Linux LVM与磁盘配额

目录 一.LVM概述 LVM LVM机制的基本概念 PV&#xff08;Physical Volume&#xff0c;物理卷&#xff09; VG&#xff08;Volume Group&#xff0c;卷组&#xff09; LV&#xff08;Logical Volume&#xff0c;逻辑卷&#xff09; 二.LVM 的管理命令 三.创建并使用LVM …

AutoPSA中推荐用户使用仿CII计算

Q:请问未知错误。优易AUtoPSAInDon3.app1670行是什么原因呢&#xff1f;如下图&#xff1a; A: 这是老的仿GLIF算法&#xff0c;遇到特殊情况有这个提示&#xff0c;建议使用仿CAESARII算法。

车载摄像头视频防抖处理解决方案,全新的稳定视觉体验

面对复杂多变的道路环境和车辆运动状态&#xff0c;如何确保车载摄像头拍摄的视频稳定清晰&#xff0c;一直是行业面临的重要挑战。美摄科技&#xff0c;作为视频防抖技术的领军企业&#xff0c;以其领先的车载摄像头视频防抖处理解决方案&#xff0c;为企业提供了全新的稳定视…

C++ - 文件流fstream

C 文件流是指在C编程中使用的用于文件输入输出操作的机制。这种机制允许程序员以类似于流的方式读取和写入文件数据。在C中&#xff0c;文件流通常使用<fstream>头文件提供的类来实现。 常用的文件流类包括&#xff1a; 1. std::ofstream&#xff1a;用于向文件中写入数…

筑牢个人信息安全防线,海云安受邀参加武汉“名家论坛”国家安全教育日专题讲座

近日&#xff0c;武汉“名家论坛”国家安全教育日专题讲座活动《“刷脸”有风险&#xff0c;如何保护我们的个人信息安全&#xff1f;》在武汉图书馆报告厅举办&#xff0c;海云安副总工程师李博士受邀参加本次活动。 活动以线下讲座、线上直播的形式&#xff0c;结合“普法讲座…

rk3588 安卓调试

rknn装上了android系统&#xff0c;用type-c usb连接上电脑&#xff0c;设备管理器发现了rk3588&#xff0c;但是Android Studio没有发现设备 后来怀疑是驱动没有安装&#xff0c;我用的驱动下载地址&#xff1a; 瑞芯微Rockchip驱动安装助手(适用于RK3308 RK3399等) Mcuzone…

H2O-3机器学习平台源码编译的各种坑

H2O-3机器学习平台是一个非常适合非专业人士学习机器学习的平台&#xff0c;自带WebUI&#xff0c;效果还是蛮不错的&#xff0c;官方也提供了jar包&#xff0c;一条命令就能直接运行&#xff0c;非常方便&#xff0c;但最近有源码编译的需求&#xff0c;实际操作过程中&#x…

error: failed to push some refs to ‘https://gitee.com/zhao-zhimin12/gk.git‘

git push origin master发现以下报错: 解决办法: 一、强制推送 git push origin master -f &#xff08;加上 -f 就是强制&#xff09; 二、 先拉取最新代码&#xff0c;再推送 1.git pull origin master 2.git push origin master

OpenHarmony实战开发-如何使用ArkUIstack 组件实现多层级轮播图。

介绍 本示例介绍使用ArkUIstack 组件实现多层级轮播图。该场景多用于购物、资讯类应用。 效果图预览 使用说明 1.加载完成后显示轮播图可以左右滑动。 实现思路 1.通过stack和offsetx实现多层级堆叠。 Stack() {LazyForEach(this.swiperDataSource, (item: SwiperData, i…

基于51单片机的心形流水灯设计

基于51单片机的心形流水灯 &#xff08;仿真&#xff0b;程序&#xff0b;原理图&#xff0b;设计报告&#xff09; 功能介绍 具体功能&#xff1a; 1.采用51单片机做为主控制器&#xff1b; 2.32个彩色&#xff2c;&#xff25;&#xff24;接在单片机的32个双向&#xff29…

机器人码垛机的技术特点与应用

随着科技的飞速发展&#xff0c;机器人技术正逐渐渗透到各个行业领域&#xff0c;其中&#xff0c;机器人码垛机在物流行业的应用尤为引人瞩目。它不仅提高了物流效率&#xff0c;降低了成本&#xff0c;更在改变传统物流模式的同时&#xff0c;为行业发展带来了重大的变革。 一…

通过钉钉发送消息

1、通过钉钉群添加一个机器人 2、代码实现 /*** 发钉钉审核.** param*/private void sendDingDing(PoMaster poMaster){if(poMaster.getTotalPrice().doubleValue() > 500){String url "https://oapi.dingtalk.com/robot/send?access_tokene11bbb694ad4425bf687d2e…

冯喜运:4.17昨日黄金完美区间多空通杀,今日黄金原油分析

【黄金走势分析 】&#xff1a;黄金昨日整体过山车&#xff0c;早盘黄金冲高2392一线后回落&#xff0c;价格在2379-2389区间震荡&#xff0c;午后区间下移&#xff0c;价格在2362-2380继续震荡&#xff0c;晚间价格再次触及2363支撑反弹&#xff0c;连阳上升突破早间高点&…

数据结构速成--栈

由于是速成专题&#xff0c;因此内容不会十分全面&#xff0c;只会涵盖考试重点&#xff0c;各学校课程要求不同 &#xff0c;大家可以按照考纲复习&#xff0c;不全面的内容&#xff0c;可以看一下小编主页数据结构初阶的内容&#xff0c;找到对应专题详细学习一下。 目录 一…

信号量理论

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 理论 信号量是对公共资源的一种预定机制&#xff0c;资源不一定非要持有才算自己的&#xff0c;预定了也算&#xff0c;在未来任意时刻&#xff0c;仍然可以使用。 像我们申请有一块共享内存&#xff0c;如果一个进程正在使…

HTML5 <video> 标签属性、API 方法、事件、自定义样式详解与实用示例

HTML5 <video> 标签为网页内嵌视频提供了强大且便捷的功能。以下是对 <video> 标签的主要属性、API 方法、事件、自定义样式及其使用示例的详细介绍&#xff1a; 一、属性 1. src 定义&#xff1a;指定视频文件的 URL。示例&#xff1a;<video src"my_v…

树莓派驱动开发--iic篇(JY901S陀螺仪的三轴角度简单读取)

前言&#xff1a;既然大家都到了这步&#xff0c;想必对驱动开发有着一定的理解啦吧&#xff01;&#xff01;那我在前面说一下流程&#xff1a; 修改编译设备树》》》编写编译驱动文件》》》编写编译app文件》》》ftp挂载将前面3复制到树莓派的对应位置》》》加载驱动模块》》…

代码随想录训练营Day 24|Python|Leetcode|93.复原IP地址, 78.子集,90.子集II

93.复原IP地址 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址&#xff0c;但是 &q…

✌粤嵌—2024/4/3—合并K个升序链表✌

代码实现&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode* merge(struct ListNode *l1, struct ListNode *l2) {if (l1 NULL) {return l2;}if (l2 NULL) {return l1;}struct Lis…