JavaWeb复习 高哥的笔记(^_^)
前言 由于看得很快,有些技术不熟练,故写下此篇供自己复习
Maven 介绍 Maven是Apache旗下的一个开源项目,是一款用于管理和构建java项目的工具。
官网:https://maven.apache.org/
Apache 软件基金会,成立于1999年7月,是目前世界上最大的最受欢迎的开源软件基金会,也是一个专门为支持开源项目而生的非盈利性组织。
开源项目:https://www.apache.org/index.html#projects-list
作用 1.管理依赖 方便快捷的管理项目依赖的资源(jar包),避免版本冲突问题 当使用maven进行项目依赖(jar包)管理,则很方便的可以解决这个问题。 我们只需要在maven项目的pom.xml文件中,添加一段如下图所示的配置即可实现。
2.统一项目结构 在项目开发中,使用不同的开发工具,项目结构会不同。
若我们创建的是一个maven工程,就可以帮我们生成自动统一、标准的项目结构
具体的统一结构如下:
目录说明:
src/main/java: java源代码目录 src/main/resources: 配置文件信息 src/test/java: 测试代码 src/test/resources: 测试配置文件信息 3.项目构造 提供了标准的、跨平台的自动化项目构造方式 提供了一套简单的命令进行代码的操作 清理、编译、测试、打包、发布 Maven 模型 1). 构建生命周期/阶段(Build lifecycle & phases)
以上图中紫色框起来的部分,就是用来完成标准化构建流程 。当我们需要编译,Maven提供了一个编译插件供我们使用;当我们需要打包,Maven就提供了一个打包插件供我们使用等。
2). 项目对象模型 (Project Object Model)
以上图中紫色框起来的部分属于项目对象模型,就是将我们自己的项目抽象成一个对象模型 ,有自己专属的坐标,如下图所示是一个Maven项目:
坐标,就是资源(jar包)的唯一标识,通过坐标可以定位到所需资源(jar包)位置
3). 依赖管理模型(Dependency)
以上图中紫色框起来的部分属于依赖管理模型,是使用坐标来描述当前项目依赖哪些第三方jar包
Maven仓库 本地仓库
中央仓库
远程仓库
Maven的安装 官网下载解压,配置仓库,修改配置文件,配置环境变量
idea 全局设置maven的安装路径,配置文件,仓库路径,修改maven编译版本
Maven项目 1.先创建一个空项目
2.创建Java模块
POM配置详细 结论: Maven是一款管理和构建java项目的工具
Spring项目本周就是一个maven工程
Swagger篇 1.坐标
1 2 3 4 5 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-openapi3-jakarta-spring-boot-starter</artifactId > <version > 4.4.0</version > </dependency >
2.yml配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 springdoc: swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: alpha api-docs: path: /v3/api-docs group-configs: - group: 'all' paths-to-match: '/**' packages-to-scan: com.quick.controller knife4j: enable: true setting: language: zh_cn
3.定义配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { protected void addResourceHandlers (ResourceHandlerRegistry registry) { log.info("开始设置静态资源映射..." ); registry.addResourceHandler("/doc.html" ).addResourceLocations("classpath:/META-INF/resources/" ); registry.addResourceHandler("/webjars/**" ).addResourceLocations("classpath:/META-INF/resources/webjars/" ); } }
4.分组展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Configuration public class SwaggerConfig { @Bean public GroupedOpenApi adminApi () { return GroupedOpenApi.builder() .group("管理端接口" ) .pathsToMatch("/admin/**" ) .build(); } @Bean public GroupedOpenApi userApi () { return GroupedOpenApi.builder() .group("C端接口" ) .pathsToMatch("/user/**" ) .build(); } @Bean public OpenAPI springShopOpenAPI () { return new OpenAPI () .info(new Info () .title("接口文档" ) .description("接口文档" ) .version("v1" ) .contact(new Contact ().name("bluefoxyu" )) .license(new License ().name("Apache 2.0" ).url("http://springdoc.org" ))); } }
5.常用注解(swagger2和swagger3的区别)
参考:SpringBoot3+支持Knife4j 4.0以上_knife4j-openapi3-jakarta-spring-boot-starter-CSDN博客
SpringBootWeb 创建SpringBoot工程(需要联网) 基于Spring官方骨架,创建SpringBoot工程。
基本信息描述完毕之后,勾选web开发相关依赖。
点击Finish之后,就会联网创建这个SpringBoot工程,创建好之后,结构如下:
(也可以创建空项目,需要再加注解和依赖)
==注意:在联网创建过程中,会下载相关资源(请耐心等待)==
创建项目工程目录结构:
配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
application.properties (直接把之前项目中的复制过来) 1 2 3 4 5 6 7 8 9 10 11 spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/tlias spring.datasource.username =root spring.datasource.password =1234 mybatis.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl mybatis.configuration.map-underscore-to-camel-case =true
springboot自带Tomcat基于http协议
原理 Tomcat是不识别我们自定义的Controller的,但Tomcat是一个Servlet容器,支持Serlvet规范。
SpringBoot在进行web程序开发时,内置了个核心的Servlet程序DispatchServlet,称之为核心控制器。它负责接收页面的请求,然后执行的规则,转发给处理器Controller,在处理器处理完请求后,再由其给浏览器响应数据
那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达tomcat之后,tomcat会负责解析这些请求数据,然后呢将解析后的请求数据会传递给Servlet程序的HttpServletRequest对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而Tomcat,还给Servlet程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。
请求 数组:接收时可以直接定义数组类型
集合:需要加@RequestParam绑定参数关系
日期:对日期类型进行封装时,需要通过@DateTimeFormat注解设置格式
JSON:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。
路径:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数
响应 我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?
@RestController = @Controller + @ResponseBody
为了统一规范,给前端方便
统一的返回结果使用类来描述,在这个结果中包含:
定义在一个实体类Result来包含以上信息。代码如下:
分层解耦 通过IOC和DI的入门程序呢,我们已经基本了解了IOC和DI的基础操作。接下来呢,我们学习下IOC控制反转和DI依赖注入的细节。
bean的声明 前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:
@Controller (标注在控制层类上) @Service (标注在业务层类上) @Repository (标注在数据访问层类上) di详细 依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。
@Autowired注解,默认是按照类型 进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)
那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢?
如何解决上述问题呢?Spring提供了以下几种解决方案:
@Primary
@Qualifier
@Resource
面试题 : @Autowird 与 @Resource的区别
@Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解 @Autowired 默认是按照类型注入,而@Resource是按照名称注入 开发规范 了解完需求也完成了环境搭建了,我们下面开始学习开发的一些规范。
开发规范我们主要从以下几方面介绍:
1、开发规范-REST
我们的案例是基于当前最为主流的前后端分离模式进行开发。
在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发。
后端开发人员:必须严格遵守提供的接口文档进行后端功能开发(保障开发的功能可以和前端对接)
而在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。
什么是REST风格呢?
REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。 传统URL风格如下:
1 2 3 4 http://localhost:8080/user/getById?id=1 GET:查询id为1的用户 http://localhost:8080/user/saveUser POST:新增用户 http://localhost:8080/user/updateUser POST:修改用户 http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户
我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。
基于REST风格URL如下:
1 2 3 4 http://localhost:8080/users/1 GET:查询id为1的用户 http://localhost:8080/users POST:新增用户 http://localhost:8080/users PUT:修改用户 http://localhost:8080/users/1 DELETE:删除id为1的用户
其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。
GET : 查询 POST :新增 PUT :修改 DELETE :删除 我们看到如果是基于REST风格,定义URL,URL将会更加简洁、更加规范、更加优雅。
注意事项:
REST是风格,是约定方式,约定不是规定,可以打破 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books… Git和MySQL安装 本来是不想写这个的,因为之前单独学过 现在是换电脑了,没有MySQL和git,直接附上安装教程
MySQL安装教程(详细版)_mysql安装教程8.0.36-CSDN博客
同时将bin文件夹配到环境变量中,路径:C:\Program Files\MySQL\MySQL Server 8.0\bin
Git 详细安装教程(详解 Git 安装过程的每一个步骤)_git安装-CSDN博客
git已经自动配好环境变量,不需要手动了
idea中设置记住Git账号密码《新版》_idea记住git密码-CSDN博客
注:登录过一次会自动保存密码,可以不理会
文件上传 OSS 从官网查找,改成工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Data @AllArgsConstructor public class AliOssUtil { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; public String upload (MultipartFile file) throws Exception { InputStream inputStream = file.getInputStream(); String originalFilename = file.getOriginalFilename(); String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("." )); OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); try { PutObjectRequest putObjectRequest = new PutObjectRequest (bucketName, fileName, inputStream); PutObjectResult result = ossClient.putObject(putObjectRequest); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason." ); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network." ); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null ) { ossClient.shutdown(); } } String url = endpoint.split("//" )[0 ] + "//" + bucketName + "." + endpoint.split("//" )[1 ] + "/" + fileName; return url; } }
添加一Properties类,方便获取账号密码
1 2 3 4 5 6 7 8 9 10 11 12 @Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
若该类不在启动类同包,则需要添加配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil (AliOssProperties aliOssProperties) { log.info("开始创建阿里云文件上传工具类对象:{}" ,aliOssProperties); return new AliOssUtil (aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }
为了方便参数变动,所以不会直接在代码中写死配置信息,我们可以将参数配置在配置文件中。如下:
1 2 3 4 5 aliyun.oss.endpoint =https://oss-cn-hangzhou.aliyuncs.com aliyun.oss.accessKeyId =LTAI4GCH1vX6DKqJWxd6nEuW aliyun.oss.accessKeySecret =yBshYweHOpqDuhCArrVHwIiBKpyqSL aliyun.oss.bucketName =web-tlias
配置有两种方式注入,如图
一、
二、
需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致
比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法
需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象
在实体类上添加@ConfigurationProperties
注解,并通过prefix属性来指定配置参数项的前缀
同时还需要加入依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > </dependency >
注意:那么如果需要上传大文件,可以在application.properties进行如下配置:
1 2 3 4 5 spring.servlet.multipart.max-file-size =10MB spring.servlet.multipart.max-request-size =100MB
登录篇(JWT令牌) JWT的组成: JWT令牌由三个部分组成,三个部分之间使用英文的点来分割
第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:”HS256”,”type”:”JWT”}
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:”1”,”username”:”Tom”}
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。
生成和校验 依赖 1 2 3 4 5 6 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency >
JWT代码实返回 1 2 3 4 5 6 7 8 9 10 11 12 if (loginEmp !=null ){ Map<String , Object> claims = new HashMap <>(); claims.put("id" , loginEmp.getId()); claims.put("username" ,loginEmp.getUsername()); claims.put("name" ,loginEmp.getName()); String token = JwtUtils.generateJwt(claims); return Result.success(token); }
JWT工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class JwtUtil { public static String createJWT (String secretKey, long ttlMillis, Map<String, Object> claims) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date (expMillis); JwtBuilder builder = Jwts.builder() .setClaims(claims) .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) .setExpiration(exp); return builder.compact(); } public static Claims parseJWT (String secretKey, String token) { Claims claims = Jwts.parser() .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token).getBody(); return claims; } }
登录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Autowired private JwtProperties jwtProperties;@PostMapping("/login") @ApiOperation("员工登录") public Result<EmployeeLoginVO> login (@RequestBody EmployeeLoginDTO employeeLoginDTO) { log.info("员工登录:{}" , employeeLoginDTO); Employee employee = employeeService.login(employeeLoginDTO); 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); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public Employee login (EmployeeLoginDTO employeeLoginDTO) { String username = employeeLoginDTO.getUsername(); String password = employeeLoginDTO.getPassword(); Employee employee = employeeMapper.getByUsername(username); if (employee == null ) { throw new AccountNotFoundException (MessageConstant.ACCOUNT_NOT_FOUND); } 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); } return employee; }
Filter过滤器 快速入门 什么是Filter?
Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
下面我们通过Filter快速入门程序掌握过滤器的基本使用操作:
第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。 过滤器链 所谓过滤器链指的是在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。
比如:在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。
而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再来执行第二个Filter,如果执行到了最后一个过滤器放行之后,才会访问对应的web资源。
访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。
登录校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @Slf4j @WebFilter(urlPatterns = "/*") public class LoginCheckFilter implements Filter { @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String url = request.getRequestURL().toString(); log.info("请求路径:{}" , url); if (url.contains("/login" )){ chain.doFilter(request, response); return ; } String token = request.getHeader("token" ); log.info("从请求头中获取的令牌:{}" ,token); if (!StringUtils.hasLength(token)){ log.info("Token不存在" ); Result responseResult = Result.error("NOT_LOGIN" ); String json = JSONObject.toJSONString(responseResult); response.setContentType("application/json;charset=utf-8" ); response.getWriter().write(json); return ; } try { JwtUtils.parseJWT(token); }catch (Exception e){ log.info("令牌解析失败!" ); Result responseResult = Result.error("NOT_LOGIN" ); String json = JSONObject.toJSONString(responseResult); response.setContentType("application/json;charset=utf-8" ); response.getWriter().write(json); return ; } chain.doFilter(request, response); } }
在上述过滤器的功能实现中,我们使用到了一个第三方json处理的工具包fastjson。我们要想使用,需要引入如下依赖:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.76</version > </dependency >
Interceptor拦截器 快速入门 什么是拦截器?
是一种动态拦截方法调用的机制,类似于过滤器。 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。 拦截器的作用:
拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。 在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。
下面我们通过快速入门程序,来学习下拦截器的基本使用。拦截器的使用步骤和过滤器类似,也分为两步:
定义拦截器 注册配置拦截器 执行流程 介绍完拦截路径的配置之后,接下来我们再来介绍拦截器的执行流程。通过执行流程,大家就能够清晰的知道过滤器与拦截器的执行时机。
当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()
方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。
在controller当中的方法执行完毕之后,再回过来执行postHandle()
这个方法以及afterCompletion()
方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。
过滤器和拦截器之间的区别,其实它们之间的区别主要是两点:
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。 登录校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true ; } String token = request.getHeader(jwtProperties.getAdminTokenName()); 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); BaseContext.setCurrentId(empId); return true ; } catch (Exception ex) { response.setStatus(401 ); return false ; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; 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" ); } }
异常处理 问题 我们来看一下出现异常之后,最终服务端给前端响应回来的数据长什么样。
响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据。
接下来我们需要思考的是出现异常之后,当前案例项目的异常是怎么处理的?
当我们没有做任何的异常处理时,我们三层架构处理异常的方案:
Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。 service 中也存在异常了,会抛给controller。 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。 解决方式 那么在三层构架项目中,出现了异常,该如何处理?
方案一:在所有Controller的所有方法中进行try…catch处理 方案二:全局异常处理器
定义全局异常处理器 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。 1 2 3 4 5 6 7 8 9 10 11 12 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result ex (Exception e) { e.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员" ); } }
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
事务 定义 事务 是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。
怎么样来控制这组操作,让这组操作同时成功或同时失败呢?此时就要涉及到事务的具体操作了。
事务的操作主要有三步:
开启事务(一组操作开始前,开启事务):start transaction / begin ; 提交事务(这组操作全部成功后,提交事务):commit ; 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ; Transactional注解 @Transactional注解书写位置:
方法 类 接口接口下所有的实现类当中所有的方法都交给spring 进行事务管理 注意:在业务功能上添加@Transactional注解进行事务管理后,我们重启SpringBoot服务
说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
1 2 3 4 logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
进阶 前面我们通过spring事务管理注解@Transactional已经控制了业务层方法的事务。接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional注解当中的两个常见的属性:
1.异常回滚的属性:rollbackFor 假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
2.事务传播行为:propagation 介绍 所谓事务的传播行为,指的就是在A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候新建一个事务?这个就涉及到了事务的传播行为。
我们要想控制事务的传播行为,在@Transactional注解的后面指定一个属性propagation,通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。
属性值 含义 REQUIRED 【默认值】需要事务,有则加入,无则创建新事务 REQUIRES_NEW 需要新事务,无论有无,总是创建新事务 SUPPORTS 支持事务,有则加入,无则在无事务状态中运行 NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 MANDATORY 必须有事务,否则抛异常 NEVER 必须没事务,否则抛异常 …
对于这些事务传播行为,我们只需要关注以下两个就可以了:
REQUIRED(默认值) REQUIRES_NEW 案例 接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。
需求: 解散部门时需要记录操作日志
由于解散部门是一个非常重要而且非常危险的操作,所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹,就是要记录操作日志。而且还要求无论是执行成功了还是执行失败了,都需要留下痕迹。
步骤:
执行解散部门的业务:先删除部门,再删除部门下的员工(前面已实现) 记录解散部门的日志,到日志表(未实现) 准备工作:
创建数据库表 dept_log 日志表: 1 2 3 4 5 create table dept_log( id int auto_increment comment '主键ID' primary key, create_time datetime null comment '操作时间', description varchar(300) null comment '操作描述' )comment '部门操作日志表';
引入资料中提供的实体类:DeptLog 1 2 3 4 5 6 7 8 @Data @NoArgsConstructor @AllArgsConstructor public class DeptLog { private Integer id; private LocalDateTime createTime; private String description; }
引入资料中提供的Mapper接口:DeptLogMapper 1 2 3 4 5 6 7 @Mapper public interface DeptLogMapper { @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})") void insert (DeptLog log) ; }
引入资料中提供的业务接口:DeptLogService 1 2 3 public interface DeptLogService { void insert (DeptLog deptLog) ; }
引入资料中提供的业务实现类:DeptLogServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 @Service public class DeptLogServiceImpl implements DeptLogService { @Autowired private DeptLogMapper deptLogMapper; @Transactional @Override public void insert (DeptLog deptLog) { deptLogMapper.insert(deptLog); } }
代码实现:
业务实现类:DeptServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; @Autowired private DeptLogService deptLogService; @Override @Log @Transactional(rollbackFor = Exception.class) public void delete (Integer id) throws Exception { try { deptMapper.deleteById(id); if (true ){ throw new Exception ("出现异常了~~~" ); } empMapper.deleteByDeptId(id); }finally { DeptLog deptLog = new DeptLog (); deptLog.setCreateTime(LocalDateTime.now()); deptLog.setDescription("执行了解散部门的操作,此时解散的是" +id+"号部门" ); deptLogService.insert(deptLog); } } }
测试:
重新启动SpringBoot服务,测试删除3号部门后会发生什么?
执行了删除3号部门操作 执行了插入部门日志操作 程序发生Exception异常 执行事务回滚(删除、插入操作因为在一个事务范围内,两个操作都会被回滚)
然后在dept_log表中没有记录日志数据
原因分析:
接下来我们就需要来分析一下具体是什么原因导致的日志没有成功的记录。
解决方案:
在DeptLogServiceImpl类中insert方法上,添加@Transactional(propagation = Propagation.REQUIRES_NEW)
Propagation.REQUIRES_NEW :不论是否有事务,都创建新事务 ,运行在一个独立的事务中。
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class DeptLogServiceImpl implements DeptLogService { @Autowired private DeptLogMapper deptLogMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insert (DeptLog deptLog) { deptLogMapper.insert(deptLog); } }
重启SpringBoot服务,再次测试删除3号部门:
那此时,DeptServiceImpl中的delete方法运行时,会开启一个事务。 当调用 deptLogService.insert(deptLog) 时,也会创建一个新的事务,那此时,当insert方法运行完毕之后,事务就已经提交了。 即使外部的事务出现异常,内部已经提交的事务,也不会回滚了,因为是两个独立的事务。
到此事务传播行为已演示完成,事务的传播行为我们只需要掌握两个:REQUIRED、REQUIRES_NEW。
AOP(用到再看资料) 问题 对于一个项目来讲,里面会包含很多的业务模块,每个业务模块又包含很多增删改查的方法,如果我们要在每一个模块下的业务方法中,添加记录开始时间、结束时间、计算执行耗时的代码,就会让程序员的工作变得非常繁琐。
而AOP面向方法编程,就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
我们要想完成统计各个业务方法执行耗时的需求,我们只需要定义一个模板方法,将记录方法执行耗时这一部分公共的逻辑代码,定义在模板方法当中,在这个方法开始运行之前,来记录这个方法运行的开始时间,在方法结束运行的时候,再来记录方法运行的结束时间,中间就来运行原始的业务方法。
而中间运行的原始业务方法,可能是其中的一个业务方法,比如:我们只想通过 部门管理的 list 方法的执行耗时,那就只有这一个方法是原始业务方法。 而如果,我们是先想统计所有部门管理的业务方法执行耗时,那此时,所有的部门管理的业务方法都是 原始业务方法。 那面向这样的指定的一个或多个方法进行编程,我们就称之为 面向切面编程。
AOP的优势:
减少重复代码 提高开发效率 维护方便 实现步骤 导入依赖:在pom.xml中导入AOP的依赖 编写AOP程序:针对于特定方法根据业务需要进行编程 pom.xml
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
核心概念 1. 连接点:JoinPoint ,可以被AOP控制的方法(暗含方法执行时的相关信息)
2. 通知:Advice ,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
3. 切入点:PointCut ,匹配连接点的条件,通知仅会在切入点方法执行时被应用
4. 切面:Aspect ,描述通知与切入点的对应关系(通知+切入点)
5. 目标对象:Target ,通知所应用的对象
原理
进阶 Spring中AOP的通知类型: @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行 @Before:前置通知,此注解标注的通知方法在目标方法前被执行 @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行 五种常见的通知类型,我们已经测试完毕了,此时我们再来看一下刚才所编写的代码,有什么问题吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Before("execution(* com.itheima.service.*.*(..))") @Around("execution(* com.itheima.service.*.*(..))") @After("execution(* com.itheima.service.*.*(..))") @AfterReturning("execution(* com.itheima.service.*.*(..))") @AfterThrowing("execution(* com.itheima.service.*.*(..))")
我们发现啊,每一个注解里面都指定了切入点表达式,而且这些切入点表达式都一模一样。此时我们的代码当中就存在了大量的重复性的切入点表达式,假如此时切入点表达式需要变动,就需要将所有的切入点表达式一个一个的来改动,就变得非常繁琐了。
怎么来解决这个切入点表达式重复的问题? 答案就是:抽取
Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
1 2 3 4 5 6 7 8 9 10 @Pointcut("execution(* com.itheima.service.*.*(..))") private void pt () {} @Before("pt()") public void before (JoinPoint joinPoint) { log.info("before ..." ); }
通知顺序 通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行 目标方法后的通知方法:字母排名靠前的后执行 如果我们想控制通知的执行顺序有两种方式:
修改切面类的类名(这种方式非常繁琐、而且不便管理)
使用Spring提供的@Order注解
切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
切入点表达式 切入点表达式:
描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
execution(……):根据方法的签名来匹配
@annotation(……) :根据注解匹配
首先我们先学习第一种最为常见的execution切入点表达式。
execution execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1 execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的表示可以省略的部分
示例:
1 @Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
切入点表达式的语法规则:
方法的访问修饰符可以省略 返回值可以使用*
号代替(任意返回值类型) 包名可以使用*
号代替,代表任意包(一层包使用一个*
) 使用..
配置包名,标识此包以及此包下的所有子包 类名可以使用*
号代替,标识任意类 方法名可以使用*
号代替,表示任意方法 可以使用 *
配置参数,一个任意类型的参数 可以使用..
配置参数,任意个任意类型的参数 @annotation 实现步骤:
编写自定义注解
在业务类要做为连接点的方法上添加自定义注解
自定义注解 :MyLog
1 2 3 4 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog {}
业务类 :DeptServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override @MyLog public List<Dept> list () { List<Dept> deptList = deptMapper.list(); return deptList; } @Override @MyLog public void delete (Integer id) { deptMapper.delete(id); } @Override public void save (Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } @Override public Dept getById (Integer id) { return deptMapper.getById(id); } @Override public void update (Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept); } }
切面类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Slf4j @Component @Aspect public class MyAspect6 { @Before("@annotation(com.itheima.anno.MyLog)") public void before () { log.info("MyAspect6 -> before ..." ); } @After("@annotation(com.itheima.anno.MyLog)") public void after () { log.info("MyAspect6 -> after ..." ); } }
连接点 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
苍穹应用篇(公共字段自动填) 1.3.1 步骤一 自定义注解 AutoFill
进入到sky-server模块,创建com.sky.annotation包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 () ; }
其中OperationType已在sky-common模块中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.sky.enumeration;public enum OperationType { UPDATE, INSERT }
1.3.2 步骤二 自定义切面 AutoFillAspect
在sky-server模块,创建com.sky.aspect包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.sky.aspect;@Aspect @Component @Slf4j public class AutoFillAspect { @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut () {} @Before("autoFillPointCut()") public void autoFill (JoinPoint joinPoint) { log.info("开始进行公共字段自动填充..." ); } }
完善自定义切面 AutoFillAspect 的 autoFill 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 package com.sky.aspect;import com.sky.annotation.AutoFill;import com.sky.constant.AutoFillConstant;import com.sky.context.BaseContext;import com.sky.enumeration.OperationType;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;import java.lang.reflect.Method;import java.time.LocalDateTime;@Aspect @Component @Slf4j public class AutoFillAspect { @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut () {} @Before("autoFillPointCut()") public void autoFill (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){ 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) { e.printStackTrace(); } }else if (operationType == OperationType.UPDATE){ 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) { e.printStackTrace(); } } } }
1.3.3 步骤三 在Mapper接口的方法上加入 AutoFill 注解
以CategoryMapper 为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper 做相同操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.sky.mapper;@Mapper public interface CategoryMapper { @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" + " VALUES" + " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") @AutoFill(value = OperationType.INSERT) void insert (Category category) ; @AutoFill(value = OperationType.UPDATE) void update (Category category) ; }
同时 ,将业务层为公共字段赋值的代码注释掉。
1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。
2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。
Redis 简介 1). 导入Spring Data Redis的maven坐标(已完成)
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
2). 配置Redis数据源
在application-dev.yml中添加
1 2 3 4 5 6 sky: redis: host: localhost port: 6379 password: 123456 database: 10
解释说明:
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。
可以通过修改Redis配置文件来指定数据库的数量。
在application.yml中添加读取application-dev.yml中的相关Redis配置
1 2 3 4 5 6 7 8 spring: profiles: active: dev redis: host: ${sky.redis.host} port: ${sky.redis.port} password: ${sky.redis.password} database: ${sky.redis.database}
3). 编写配置类,创建RedisTemplate对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.sky.config;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate (RedisConnectionFactory redisConnectionFactory) { log.info("开始创建redis模板对象..." ); RedisTemplate redisTemplate = new RedisTemplate (); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer ()); return redisTemplate; } }
解释说明:
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为
StringRedisSerializer序列化器。
缓存菜品代码 修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Autowired private RedisTemplate redisTemplate; @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list (Long categoryId) { String key = "dish_" + categoryId; List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if (list != null && list.size() > 0 ){ return Result.success(list); } Dish dish = new Dish (); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE); list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key, list); return Result.success(list); }
为了保证数据库 和Redis 中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。
需要改造的方法:
1 redisTemplate.delete(keys);
HttpClient 简介 坐标
1 2 3 4 5 <dependency > <groupId > org.apache.httpcomponents</groupId > <artifactId > httpclient</artifactId > <version > 4.5.13</version > </dependency >
HttpClient的核心API:
HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。 HttpClients:可认为是构建器,可创建HttpClient对象。 CloseableHttpClient:实现类,实现了HttpClient接口。 HttpGet:Get方式请求类型。 HttpPost:Post方式请求类型。 HttpClient发送请求步骤:
创建HttpClient对象 创建Http请求对象 调用HttpClient的execute方法发送请求 Get方式请求 实现步骤:
创建HttpClient对象 创建请求对象 发送请求,接受响应结果 解析结果 关闭资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class HttpClientTest { @Test public void testGET () throws Exception{ CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet ("http://localhost:8080/user/shop/status" ); CloseableHttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); System.out.println("服务端返回的状态码为:" + statusCode); HttpEntity entity = response.getEntity(); String body = EntityUtils.toString(entity); System.out.println("服务端返回的数据为:" + body); response.close(); httpClient.close(); } }
POST方式请求 实现步骤:
创建HttpClient对象 创建请求对象 发送请求,接收响应结果 解析响应结果 关闭资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Test public void testPOST () throws Exception{ CloseableHttpClient httpClient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost ("http://localhost:8080/admin/employee/login" ); JSONObject jsonObject = new JSONObject (); jsonObject.put("username" ,"admin" ); jsonObject.put("password" ,"123456" ); StringEntity entity = new StringEntity (jsonObject.toString()); entity.setContentEncoding("utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); CloseableHttpResponse response = httpClient.execute(httpPost); int statusCode = response.getStatusLine().getStatusCode(); System.out.println("响应码为:" + statusCode); HttpEntity entity1 = response.getEntity(); String body = EntityUtils.toString(entity1); System.out.println("响应数据为:" + body); response.close(); httpClient.close(); }
小程序登录 微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
流程图:
步骤分析:
小程序端,调用wx.login()获取code,就是授权码。 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。 小程序端,收到自定义登录态,存储storage。 小程序端,后绪通过wx.request()发起业务请求时,携带token。 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。 接下来,我们使用Postman进行测试。
说明:
调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID (若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key 。 之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Service @Slf4j public class UserServiceImpl implements UserService { public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session" ; @Autowired private WeChatProperties weChatProperties; @Autowired private UserMapper userMapper; public User wxLogin (UserLoginDTO userLoginDTO) { String openid = getOpenid(userLoginDTO.getCode()); if (openid == null ){ throw new LoginFailedException (MessageConstant.LOGIN_FAILED); } User user = userMapper.getByOpenid(openid); if (user == null ){ user = User.builder() .openid(openid) .createTime(LocalDateTime.now()) .build(); userMapper.insert(user); } return user; } private String getOpenid (String code) { Map<String, String> map = new HashMap <>(); map.put("appid" ,weChatProperties.getAppid()); map.put("secret" ,weChatProperties.getSecret()); map.put("js_code" ,code); map.put("grant_type" ,"authorization_code" ); String json = HttpClientUtil.doGet(WX_LOGIN, map); JSONObject jsonObject = JSON.parseObject(json); String openid = jsonObject.getString("openid" ); return openid; } }
Spring Task 介绍 Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位: 定时任务框架
作用: 定时自动执行某段Java代码
cron表达式 cron表达式 其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则: 分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
cron表达式在线生成器:https://cron.qqe2.com/
Spring Task使用步骤 1). 导入maven坐标 spring-context(已存在)
2). 启动类添加注解 @EnableScheduling 开启任务调度
3). 自定义定时任务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.sky.task;import lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.util.Date;@Component @Slf4j public class MyTask { @Scheduled(cron = "0/5 * * * * ?") public void executeTask () { log.info("定时任务开始执行:{}" ,new Date ()); } }
WebSocket 简介 WebSocket 是基于 TCP 的一种新的网络协议 。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性 的连接, 并进行双向 数据传输。
HTTP协议和WebSocket协议对比:
HTTP是短连接 WebSocket是长连接 HTTP通信是单向 的,基于请求响应模式 WebSocket支持双向 通信 HTTP和WebSocket底层都是TCP连接 快速入门 1). 定义websocket.html页面(资料中已提供)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 <!DOCTYPE HTML > <html > <head > <meta charset ="UTF-8" > <title > WebSocket Demo</title > </head > <body > <input id ="text" type ="text" /> <button onclick ="send()" > 发送消息</button > <button onclick ="closeWebSocket()" > 关闭连接</button > <div id ="message" > </div > </body > <script type ="text/javascript" > var websocket = null ; var clientId = Math .random ().toString (36 ).substr (2 ); if ('WebSocket' in window ){ websocket = new WebSocket ("ws://localhost:8080/ws/" +clientId); } else { alert ('Not support websocket' ) } websocket.onerror = function ( ){ setMessageInnerHTML ("error" ); }; websocket.onopen = function ( ){ setMessageInnerHTML ("连接成功" ); } websocket.onmessage = function (event ){ setMessageInnerHTML (event.data ); } websocket.onclose = function ( ){ setMessageInnerHTML ("close" ); } window .onbeforeunload = function ( ){ websocket.close (); } function setMessageInnerHTML (innerHTML ){ document .getElementById ('message' ).innerHTML += innerHTML + '<br/>' ; } function send ( ){ var message = document .getElementById ('text' ).value ; websocket.send (message); } function closeWebSocket ( ) { websocket.close (); } </script > </html >
2). 导入maven坐标
在sky-server模块pom.xml中已定义
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
3). 定义WebSocket服务端组件(资料中已提供)
直接导入到sky-server模块即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package com.sky.websocket;import org.springframework.stereotype.Component;import javax.websocket.OnClose;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.util.Collection;import java.util.HashMap;import java.util.Map;@Component @ServerEndpoint("/ws/{sid}") public class WebSocketServer { private static Map<String, Session> sessionMap = new HashMap (); @OnOpen public void onOpen (Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接" ); sessionMap.put(sid, session); } @OnMessage public void onMessage (String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } @OnClose public void onClose (@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } public void sendToAllClient (String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
4). 定义配置类,注册WebSocket的服务端组件(从资料中直接导入即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.sky.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
5). 定义定时任务类,定时向客户端推送数据(从资料中直接导入即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.sky.task;import com.sky.websocket.WebSocketServer;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;@Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; @Scheduled(cron = "0/5 * * * * ?") public void sendMessageToClient () { webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss" ).format(LocalDateTime.now())); } }
Apache POI Apache POI的maven坐标: 1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi</artifactId > <version > 3.16</version > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > <version > 3.16</version > </dependency >
苍穹实现 在ReportServiceImpl实现类中实现导出运营数据报表的方法:
提前将资料中的运营数据报表模板.xlsx 拷贝到项目的resources/template目录中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public void exportBusinessData (HttpServletResponse response) { LocalDate begin = LocalDate.now().minusDays(30 ); LocalDate end = LocalDate.now().minusDays(1 ); BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX)); InputStream inputStream = this .getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx" ); try { XSSFWorkbook excel = new XSSFWorkbook (inputStream); XSSFSheet sheet = excel.getSheet("Sheet1" ); sheet.getRow(1 ).getCell(1 ).setCellValue(begin + "至" + end); XSSFRow row = sheet.getRow(3 ); row.getCell(2 ).setCellValue(businessData.getTurnover()); row.getCell(4 ).setCellValue(businessData.getOrderCompletionRate()); row.getCell(6 ).setCellValue(businessData.getNewUsers()); row = sheet.getRow(4 ); row.getCell(2 ).setCellValue(businessData.getValidOrderCount()); row.getCell(4 ).setCellValue(businessData.getUnitPrice()); for (int i = 0 ; i < 30 ; i++) { LocalDate date = begin.plusDays(i); businessData = workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); row = sheet.getRow(7 + i); row.getCell(1 ).setCellValue(date.toString()); row.getCell(2 ).setCellValue(businessData.getTurnover()); row.getCell(3 ).setCellValue(businessData.getValidOrderCount()); row.getCell(4 ).setCellValue(businessData.getOrderCompletionRate()); row.getCell(5 ).setCellValue(businessData.getUnitPrice()); row.getCell(6 ).setCellValue(businessData.getNewUsers()); } ServletOutputStream out = response.getOutputStream(); excel.write(out); out.flush(); out.close(); excel.close(); }catch (IOException e){ e.printStackTrace(); } }
原理篇 暂时不感兴趣,先留着,后面填坑….