苍穹外卖学习记录(一)

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的bodyJwtBuilder 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);//设置密码,默认密码123456employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));//设置当前记录的创建时间和修改时间employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());//        通过ThreadLocal获取用户信息Long currentId = BaseContext.getCurrentId();//设置当前记录创建人id和修改人idemployee.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相关配置

在这里插入图片描述

@Beanpublic 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 {@Autowiredprivate 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存储到ThreadLocalBaseContext.setCurrentId(empId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}

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

@Component
public class ProjectInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.print("preHandle\n");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.print("postHandle\n");}@Overridepublic 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);//设置密码,默认密码123456employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));//设置当前记录的创建时间和修改时间employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());//        通过ThreadLocal获取用户信息Long currentId = BaseContext.getCurrentId();//设置当前记录创建人id和修改人idemployee.setCreateUser(currentId);//目前写个假数据,后期修改employee.setUpdateUser(currentId);//TODOemployeeMapper.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;@MyAnnotationpublic 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() {}@Overridepublic 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)@ResponseBodypublic 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 {@ExceptionHandlerpublic Result exceptionHandler(SQLIntegrityConstraintViolationException ex){//捕获的异常类型//Duplicate entry 'zhangsan' for key 'employee.idx_username'String message = ex.getMessage();//提取异常信息,就是上面的这句话if(message.contains("Duplicate entry")){//我们想将这个异常信息中的zhangsan提取出来,提示已经存在,因此便将上面的字符串进行分割,并拼接上定义好的常量提示信息:MessageConstant.ALREADY_EXISTSString[] 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插件。

 @Overridepublic PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//        开始分页查询,这是基于select * form table limit 0,10来实现的,但我们可以通过PageHelper插件来实现分页sql拼接功能。PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
//      要想使用PageHelper插件,就要符合其规则,查询的返回结果为Page类型,泛型为EmployeePage<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 {@Autowiredprivate 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存储到ThreadLocalBaseContext.setCurrentId(empId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}

2.注册拦截器

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

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

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

/*** 配置类,注册web层相关组件*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;@Autowiredprivate 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方法默认是加在最后面的,这会导致难以被使用到,因此加一个索引0converters.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>

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于机器学习/深度学习的一些事-答知乎问(三)

可解释人工智能如何进行创新&#xff1f; &#xff08;1&#xff09;解释方法结合。现有的研究较少关注如何将不同的解释方法结合起来&#xff0c;未来可以考虑将不同的 解释方法结合在一起&#xff0c;如正反结合&#xff0c;事实解释侧重于 “为什么”&#xff0c;反事实解释…

如何编写易于访问的技术文档 - 最佳实践与示例

当你为项目或工具编写技术文档时&#xff0c;你会希望它易于访问。这意味着它将为全球网络上的多样化受众提供服务并可用。 网络无障碍旨在使任何人都能访问网络内容。设计师、开发人员和撰写人员有共同的无障碍最佳实践。本文将涵盖一些创建技术内容的最佳实践。 &#xff0…

Arthas实战教程:定位Java应用CPU过高与线程死锁

引言 在Java应用开发中&#xff0c;我们可能会遇到CPU占用过高和线程死锁的问题。本文将介绍如何使用Arthas工具快速定位这些问题。 准备工作 首先&#xff0c;我们创建一个简单的Java应用&#xff0c;模拟CPU过高和线程死锁的情况。在这个示例中&#xff0c;我们将编写一个…

连接两部VR头显的type-c DP分配器方案,可以给主机设备PD反向供电与两部VR同时供电。

随着type-c的发展&#xff0c;目前越来越多的设备都在使用type-c作为连接的接口&#xff0c; 不仅是笔记本与手机在使用现在的游戏主机如&#xff08;任天堂&#xff0c;steam&#xff0c;&#xff09;或者是VR的一体机或者是VR头显也都在使用type-c作为连接接口。 type-c接口…

卷积学习笔记——一文直观形象弄懂

在神经网络的世界中,卷积操作犹如一个神秘的魔术师,它以一种精巧的方式提取出图像、声音等数据中的关键特征,为神经网络模型赋能。但究竟什么是卷积?我们一探究竟。 卷积(Convolution)本质上是一种数学运算操作,它可以用极简的数学形式漂亮地描述一个动态过程。我们可以用形象…

3D开发工具HOOPS:推动汽车行业CAD可视化发展

在最近的行业对话中&#xff0c;Tech Soft 3D&#xff08;HOOPS厂商&#xff09;的Jonathan Girroir和Actify的Peter West探讨了CAD可视化在当代企业中的重要性和挑战。作为CAD可视化领域的佼佼者&#xff0c;Actify通过其广受欢迎的Spinfire应用&#xff0c;赋能了全球40多个国…

10.哀家要长脑子了!

1. 704. 二分查找 - 力扣&#xff08;LeetCode&#xff09; 哎哟 我去 我还以为你都搞懂了 呵呵 当时问题出现在右边界初始化 左闭右开 右边界是取不到的 int left 0, right nums.size() ; while(left < right) { int mid left (right - left) / 2; if( target > …

【随笔】Git 基础篇 -- 远程仓库 git clone(二十五)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

【复习笔记】FreeRTOS(六) 队列操作

本文是FreeRTOS复习笔记的第六节&#xff0c;队列操作。 上一篇文章&#xff1a; 【复习笔记】reeRTOS(四) 列表项的插入和删除 文章目录 1.队列操作1.1.队列操作过程1.2.队列操作常用的API函数 二、实验设计三、测试例程四、实验效果 1.队列操作 队列是为了任务与任务、任务与…

IP地址的主要功能及其在网络中的重要性

在当今数字化时代&#xff0c;互联网已经成为人们生活和工作中不可或缺的一部分。而IP地址&#xff08;Internet Protocol Address&#xff09;作为互联网中的关键组成部分&#xff0c;发挥着至关重要的作用。本文将探讨IP地址的主要功能以及其在网络中的重要性。 IP地址查询&…

Xcode 15.0 新 #Preview 预览让 SwiftUI 界面调试更加悠然自得

概览 从 Xcode 15 开始&#xff0c;苹果推出了新的 #Preview 宏预览机制&#xff0c;它无论从语法还是灵活性上都远远超过之前的预览方式。#Preview 不但可以实时预览 SwiftUI 视图&#xff0c;而且对 UIKit 的界面预览也是信手拈来。 想学习新 #Preview 预览的一些超实用调试…

【GEE实践应用】按照字段提取想要的研究区域

有的时候&#xff0c;我们在GEE中加载研究区域时&#xff0c;我们现有的矢量数据可能不止自己想要的研究区域的范围&#xff0c;这个时候&#xff0c;为了避免在ArcGIS中重新导出打包上传等操作&#xff0c;我们可以在GEE中按照字段进行选择我们想要的研究区域。下面是操作实例…

杰发科技AC7840——CAN通信简介(4)_过滤器设置

0. 简介 注意&#xff1a;过滤器最高三位用不到&#xff0c;因此最高位随意设置不影响过滤器。 1. 代码分析 注意设置过滤器数量 解释的有点看不懂 详细解释...也看不大懂 Mask的第0位是0&#xff0c;其他位都是1(就是F?)&#xff0c;那就指定了接收值就是这个数&#xff0c;…

ASP.NET Core 标识(Identity)框架系列(二):使用标识(Identity)框架生成 JWT Token

前言 JWT&#xff08;JSON Web Token&#xff09;是一种开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在网络上以 JSON 对象的形式安全地传输信息。 JWT 通常用于在用户和服务器之间传递身份验证信息&#xff0c;以便在用户进行跨域访问时进行身份验证。 JWT 由…

matlab 安装 mingw64(6.3.0),OPENEXR

matlab安装openexr 1. matlab版本与对应的mingw版本选择2. mingw&#xff08;6.3.0&#xff09;下载地址&#xff1a;3. matlab2020a配置mingw&#xff08;6.3.0&#xff09;流程“4. matlab 安装openexr方法一&#xff1a;更新matlab版本方法二&#xff1a;其他博文方法方法三…

MySQL——链表

主键&#xff1a;非空 唯一&#xff08;针对整列数据而言&#xff09; 为了方便管理一般主键都是设置为自增 外键&#xff1a;一张表中的一列的值是另一张表的主键&#xff0c;使用外键建立两张数据表的数据关系 一、两张表连接 将两张表格拼接成一个表 1、格式&#xff1a;s…

爬虫 | 网易新闻热点数据的获取与保存

Hi&#xff0c;大家好&#xff0c;我是半亩花海。本项目是一个简单的网络爬虫&#xff0c;用于从网易新闻的热点新闻列表中提取标题和对应的链接&#xff0c;并将提取到的数据保存到一个 CSV 文件中。 目录 一、技术栈 二、功能说明 三、注意事项 四、代码解析 1. 导入所需…

【C++进阶】RAII思想&智能指针

智能指针 一&#xff0c;为什么要用智能指针&#xff08;内存泄漏问题&#xff09;内存泄漏 二&#xff0c;智能指针的原理2.1 RAII思想2.2 C智能指针发展历史 三&#xff0c;更靠谱的shared_ptr3.1 引用计数3.2 循环引用3.3 定制删除器 四&#xff0c;总结 上一节我们在讲抛异…

PostgreSQL入门到实战-第二十九弹

PostgreSQL入门到实战 PostgreSQL中数据分组操作(四)官网地址PostgreSQL概述PostgreSQL中CUBE命令理论PostgreSQL中CUBE命令实战更新计划 PostgreSQL中数据分组操作(四) 如何使用PostgreSQL CUBE生成多个分组集 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不…

InternlM2

第一次作业 基础作业 进阶作业 1. hugging face下载 2. 部署 首先&#xff0c;从github上git clone仓库 https://github.com/InternLM/InternLM-XComposer.git然后里面的指引安装环境