0


【代码阅读】云E办项目后端技术栈总结及源码分析

项目来源:Bilibili:带你从0搭建一个springboot+vue前后端分离的java项目

源码地址:https://github.com/Jimecc/yeb

本项目的后端部分我已经完整的部署在了我的个人服务器上,因此:

开发环境:MacBook air 2020

  • IDEA + WebStorm
  • MySQL 数据库

服务器环境:腾讯云 CentOS 8.0

个人博客地址:Jime.cc

👆前端截图

👆结构概述

后端技术栈

技术名称作用关键代码Springboot整个后端项目的框架Lombok用注解代替繁琐的操作、简化开发AutoGenerator代码生成(数据库生成最基本的pojo、mapper、service、controller 文件)Swagger2后端接口文档(多用于多人开发或前后端分离项目)JWT生成(或刷新)tokenKaptcha谷歌的验证码生成器RedisMenu 操作时,减少数据库吞吐量。EasyPOI表格操作(导出员工信息/导入员工信息并添加到数据库)RabbitMQ消息队列机制,我将需要处理的消息以队列的形式放入 RabbitMQ,随后再去读取。JavaMailJava发送邮件。@Scheduled这并不是一个技术栈,但是也准备讲一下。SpringSecuritySpringboot的安全技术,用于加密等。MyBatisPlus这个属于最基础的技术了,这里不多写了。FastDFS一个文件管理系统,主要用于存储用户头像,其优点可以查看FastDFS官网WebSocket服务器向客户端推送消息,一般都用于即时通讯工具(像是 QQ、微信、WhatsApp)

1.Springboot 关键代码

@SpringBootApplicationpublicclassYebApplication{publicstaticvoidmain(String[] args){SpringApplication.run(YebApplication.class,args);}}

👆可以说整个后端有很多地方都是 Springboot 的关键代码,因此我在这里只写了 Springboot 的启动类以及注解。

2.Lombok 关键代码

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>

以上为 pom.xml 的 dependency

@Data@NoArgsConstructor@AllArgsConstructor@RequiredArgsConstructor@EqualsAndHashCode(callSuper =false,of="name")@TableName("t_position")@ApiModel(value="Position对象", description="")publicclassPositionimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@ApiModelProperty(value ="id")@TableId(value ="id", type =IdType.AUTO)privateInteger id;@ApiModelProperty(value ="职位")@Excel(name="职位")@NonNullprivateString name;@ApiModelProperty(value ="创建时间")@JsonFormat(pattern ="yyy-MM-dd",timezone="Asia/Shanghai")privateLocalDateTime createDate;@ApiModelProperty(value ="是否启用")privateBoolean enabled;}

👆在实体类(pojo/entity)中使用@Data 取代 setter/getter方法

👆使用 @NoArgsConstructor 与 @AllArgsConstructor 分别取代无参构造与全参构造函数。

@Slf4jpublicclassAdminController{publicvoidsout(){
    log.info("这是 info 日志信息");
    log.warn("这是 warnning 日志信息");
    log.error("这是 error 日志信息");}}

👆使用 @Slf4j 生成log 日志

3.AutoGenerator 关键代码

packagecom.jim.generator;importcom.baomidou.mybatisplus.core.exceptions.MybatisPlusException;importcom.baomidou.mybatisplus.core.toolkit.StringPool;importcom.baomidou.mybatisplus.core.toolkit.StringUtils;importcom.baomidou.mybatisplus.generator.AutoGenerator;importcom.baomidou.mybatisplus.generator.InjectionConfig;importcom.baomidou.mybatisplus.generator.config.*;importcom.baomidou.mybatisplus.generator.config.po.TableInfo;importcom.baomidou.mybatisplus.generator.config.rules.NamingStrategy;importcom.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;importjava.util.ArrayList;importjava.util.List;importjava.util.Scanner;// t_admin,t_admin_role,t_appraise,t_department,t_employee,t_employee_ec,t_employee_remove,t_employee_train,t_joblevel,t_mail_log,t_menu,t_menu_role,t_nation,t_oplog,t_politics_status,t_position,t_role,t_salary,t_salary_adjust,t_sys_msg,t_sys_msg_contentpublicclassCodeGenerator{/**
     * <p>
     * 读取控制台内容
     * </p>
     */publicstaticStringscanner(String tip){Scanner scanner =newScanner(System.in);StringBuilder help =newStringBuilder();
        help.append("请输入"+ tip +":");System.out.println(help.toString());if(scanner.hasNext()){String ipt = scanner.next();if(StringUtils.isNotBlank(ipt)){return ipt;}}thrownewMybatisPlusException("请输入正确的"+ tip +"!");}publicstaticvoidmain(String[] args){// 代码生成器AutoGenerator mpg =newAutoGenerator();// 全局配置GlobalConfig gc =newGlobalConfig();finalString projectPath =System.getProperty("user.dir");
        gc.setOutputDir(projectPath +"/yeb-server/src/main/java");
        gc.setAuthor("jim");//作者
        gc.setOpen(false);//是否打开目录
        gc.setBaseResultMap(true);//xml开启BaseResultMap
        gc.setBaseColumnList(true);//xml 开启BaseColumn
        gc.setSwagger2(true);//实体属性 Swagger2 注解
        mpg.setGlobalConfig(gc);// 数据源配置DataSourceConfig dsc =newDataSourceConfig();
        dsc.setUrl("jdbc:mysql://127.0.0.1:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");// dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("12345ioS");
        mpg.setDataSource(dsc);// 包配置finalPackageConfig pc =newPackageConfig();//pc.setModuleName(scanner("模块名"));
        pc.setParent("com.jim.server").setEntity("pojo").setMapper("mapper").setService("service").setServiceImpl("service.impl").setController("controller");
        mpg.setPackageInfo(pc);// 自定义配置InjectionConfig cfg =newInjectionConfig(){@OverridepublicvoidinitMap(){// to do nothing}};// 如果模板引擎是 freemarkerString templatePath ="/templates/mapper.xml.ftl";// 如果模板引擎是 velocity// String templatePath = "/templates/mapper.xml.vm";// 自定义输出配置List<FileOutConfig> focList =newArrayList<>();// 自定义配置会被优先输出
        focList.add(newFileOutConfig(templatePath){@OverridepublicStringoutputFile(TableInfo tableInfo){// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!return projectPath +"/yeb-server/src/main/resources/mapper/"+ pc.getModuleName()+"/"+ tableInfo.getEntityName()+"Mapper"+StringPool.DOT_XML;}});/*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);// 配置模板TemplateConfig templateConfig =newTemplateConfig();// 配置自定义输出模板//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别// templateConfig.setEntity("templates/entity2.java");// templateConfig.setService();// templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);// 策略配置StrategyConfig strategy =newStrategyConfig();//数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.no_change);//strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");//lombok模型
        strategy.setEntityLombokModel(true);//生成RestController
        strategy.setRestControllerStyle(true);// 公共父类//strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");// 写于父类中的公共字段//strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);//表前缀
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(newFreemarkerTemplateEngine());
        mpg.execute();}}

👆这段代码中有独立的 main 函数,可以直接运行此类,运行后在控制台输入数据库表名称,可以直接生成

mapper、service、controller、pojo 类

,节省无效开发的时间。(本技术使用的是 MyBatis-Plus 的代码自动生成器,具体可以去 MyBatis-Plus 官网查看)

👆代码自动生成在项目中为一个单独的子项目,创建方式:在父项目中创建相一个Maven 项目,然后关联父项目。

4.Swagger2 效果/关键代码

pojo 对象

👆Swagger 对 pojo 的展示

👆Swagger 对 Controller 接口的展示

🏀 具体操作可以访问-YebSwagger接口文档-自行尝试

// Swagger2 配置文件packagecom.jim.server.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importspringfox.documentation.builders.ApiInfoBuilder;importspringfox.documentation.builders.PathSelectors;importspringfox.documentation.builders.RequestHandlerSelectors;importspringfox.documentation.service.*;importspringfox.documentation.spi.DocumentationType;importspringfox.documentation.spi.service.contexts.SecurityContext;importspringfox.documentation.spring.web.plugins.Docket;importspringfox.documentation.swagger2.annotations.EnableSwagger2;importjava.util.ArrayList;importjava.util.List;@Configuration@EnableSwagger2publicclassSwagger2Config{@BeanpublicDocketcreateRestApi(){returnnewDocket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.jim.server.controller")).paths(PathSelectors.any()).build().securityContexts(securityContexts()).securitySchemes(securitySchemes());}privateApiInfoapiInfo(){returnnewApiInfoBuilder().title("云E办接口文档").description("云E办接口文档").contact(newContact("Jim","http://localhost:8081/doc.html","[email protected]")).version("1.0").build();}privateList<ApiKey>securitySchemes(){// 设置请求头信息List<ApiKey> result =newArrayList<>();ApiKey apiKey =newApiKey("Authorization","Authorization","Header");
        result.add(apiKey);return result;}privateList<SecurityContext>securityContexts(){// 设置需要登录认证的路程List<SecurityContext> result =newArrayList<>();
        result.add(getContextByPath("/hello/.*"));return result;}privateSecurityContextgetContextByPath(String pathRegex){returnSecurityContext.builder().securityReferences(defaultAuth()).forPaths(PathSelectors.regex(pathRegex)).build();}privateList<SecurityReference>defaultAuth(){List<SecurityReference> result =newArrayList<>();AuthorizationScope authorizationScope =newAuthorizationScope("global","accessEverything");AuthorizationScope[] authorizationScopes =newAuthorizationScope[1];
        authorizationScopes[0]= authorizationScope;
        result.add(newSecurityReference("Authorization",authorizationScopes));return result;}}
@RestController@RequestMapping("/admin")publicclassAdminController{@ApiOperation(value ="获取所有操作员")@GetMapping("/")publicList<Admin>getAllAdmins(String keywords){return adminService.getAllAdmins(keywords);}}

👆在 Controller 层使用

@ApiOperation(value = "名称"

) 对接口进行命名,名称为“名称”,可以在接口文档查看的时候看到此名称,如果不写则以方法名称为默认名称(类上也可以用@Api(value=“名称”)

@ApiModel(value="Admin对象", description="")publicclassAdminimplementsSerializable,UserDetails{@ApiModelProperty(value ="id")privateInteger id;}

👆在 pojo 类中使用

@ApiModel

注解让 Swagger 知道当前内容是一个对象类,

value

为接口文档中显示的名称,

description

为接口文档中显示的简介。

👆在成员属性中用

@ApiModelProperty(value = "id")

注释描述属性的名字。

5.JWT 关键代码

packagecom.jim.server.config.security;importio.jsonwebtoken.*;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.stereotype.Component;importjava.util.Date;importjava.util.HashMap;importjava.util.Map;@ComponentpublicclassJwtTokenUtil{privatestaticfinalStringCLAIM_KEY_USERNAME="sub";privatestaticfinalStringCLAIM_KEY_CREATED="created";@Value("${jwt.secret}")privateString secret;@Value("${jwt.expiration}")privateLong expiration;/**
     * @Author: Jim
     * @Description: 根据用户信息生成 TOKEN
     * @Params:  public
     */publicStringgenerateToken(UserDetails userDetails){Map<String,Object> claims =newHashMap<>();
        claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,newDate());System.out.println(claims.toString());returngenerateToken(claims);}/**
     * @Author: Jim
     * @Description: 从 token 中获取登录用户名
     * @Params: 
     */publicStringgetUserNameFromToken(String token){String username;// 根据 token 拿一个荷载try{Claims claims =getClaimsFromToken(token);
            username = claims.getSubject();}catch(Exception e){
            username =null;}return username;}/**
     * @Author: Jim
     * @Description: 判断 token 是否有效
     * @Params:
     */publicbooleanvalidateToken(String token,UserDetails userDetails){String username =getUserNameFromToken(token);return username.equals(userDetails.getUsername())&&!isTokenExpired(token);}/**
     * @Author: Jim
     * @Description: 判断 token 是否可以刷新
     * @Params:
     */publicbooleancanRefresh(String token){return!isTokenExpired(token);}/**
     * @Author: Jim
     * @Description: 刷新 token
     * @Params:
     */publicStringrefreshToken(String token){Claims claims =getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,newDate());returngenerateToken(claims);}/**
     * @Author: Jim
     * @Description: 判断 token 是否失效
     * @Params:
     */privatebooleanisTokenExpired(String token){Date expireDate =getExpiredDateFromToken(token);return expireDate.before(newDate());}/**
     * @Author: Jim
     * @Description: 从 token 中获取过期时间
     * @Params:
     */privateDategetExpiredDateFromToken(String token){Claims claims =getClaimsFromToken(token);return claims.getExpiration();}/**
     * @Author: Jim
     * @Description: 从 token 中获取荷载
     * @Params:
     */privateClaimsgetClaimsFromToken(String token){Claims claims =null;try{
            claims =Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}catch(Exception e){
            e.printStackTrace();}return claims;}/**
     * @Author: Jim
     * @Description: 根据荷载生成 JWT TOKEN
     * @Params: 
     */privateStringgenerateToken(Map<String,Object> claims){returnJwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512,secret).compact();}/**
     * @Author: Jim
     * @Description: 生成 TOKEN 失效时间
     * @Params:
     */privateDategenerateExpirationDate(){returnnewDate(System.currentTimeMillis()+expiration*1000);}}

👆该类为 JWT 的一个基本方法类,主要使用

Jwts.builder()

生成一个 token,token 中包含一个用户名与创建时间,后续,前端访问后会将该 token 存储在 windows.session 中(也就是 httpsession)中,每次访问后端都会携带该 session,后端得到 session 也就得到了 token,可以解析出 username 与 Date,以此来判断该用户是否登录、登录是否过期。

👆顺便一提,退出登录后端不进行任何操作,仅返回一个成功码200,前端得到成功码后删除前端存储的 session 就可以了,再次使用就会提醒你要登录了。

packagecom.jim.server.config.security;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.web.authentication.WebAuthenticationDetailsSource;importorg.springframework.web.filter.OncePerRequestFilter;importjavax.servlet.FilterChain;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;publicclassJwtAuthencationTokenFilterextendsOncePerRequestFilter{@Value("${jwt.tokenHeader}")privateString tokenHeader;@Value("${jwt.tokenHead}")privateString tokenHead;@AutowiredprivateJwtTokenUtil jwtTokenUtil;@AutowiredprivateUserDetailsService userDetailsService;@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)throwsServletException,IOException{String authHeader = request.getHeader(tokenHeader);// 存在 tokenif(null!= authHeader && authHeader.startsWith(tokenHead)){String authToken = authHeader.substring(tokenHead.length());String username = jwtTokenUtil.getUserNameFromToken(authToken);// token 存在用户名但是未登录if(null!= username &&null==SecurityContextHolder.getContext().getAuthentication()){UserDetails userDetails = userDetailsService.loadUserByUsername(username);if(jwtTokenUtil.validateToken(authToken,userDetails)){UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authenticationToken.setDetails(newWebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}}}
        filterChain.doFilter(request,response);}}

🏀还用到了一些相关的过滤器,可以下载源码后查看。


6.Kaptcha 关键代码

packagecom.jim.server.config;importcom.google.code.kaptcha.impl.DefaultKaptcha;importcom.google.code.kaptcha.util.Config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.util.Properties;/**
 * 验证码配置类
 * @author Jim
 * @since 1.0.0
 */@ConfigurationpublicclassCaptchaConfig{@BeanpublicDefaultKaptchadefaultKaptcha(){//验证码生成器DefaultKaptcha defaultKaptcha =newDefaultKaptcha();//配置Properties properties =newProperties();//是否有边框
        properties.setProperty("kaptcha.border","yes");//设置边框颜色
        properties.setProperty("kaptcha.border.color","105,179,90");//边框粗细度,默认为1// properties.setProperty("kaptcha.border.thickness","1");//验证码
        properties.setProperty("kaptcha.session.key","code");//验证码文本字符颜色 默认为黑色
        properties.setProperty("kaptcha.textproducer.font.color","blue");//设置字体样式
        properties.setProperty("kaptcha.textproducer.font.names","宋体,楷体,微软雅黑");//字体大小,默认40
        properties.setProperty("kaptcha.textproducer.font.size","30");//验证码文本字符内容范围 默认为abced2345678gfynmnpwx// properties.setProperty("kaptcha.textproducer.char.string", "");//字符长度,默认为5
        properties.setProperty("kaptcha.textproducer.char.length","4");//字符间距 默认为2
        properties.setProperty("kaptcha.textproducer.char.space","4");//验证码图片宽度 默认为200
        properties.setProperty("kaptcha.image.width","100");//验证码图片高度 默认为40
        properties.setProperty("kaptcha.image.height","40");Config config =newConfig(properties);
        defaultKaptcha.setConfig(config);return defaultKaptcha;}}

👆以上为

Kaptcha

的配置类,主要操作为创建一个

DefaultKaptcha

对象,并对对象进行配置。

packagecom.jim.server.controller;importcom.google.code.kaptcha.impl.DefaultKaptcha;importio.swagger.annotations.ApiOperation;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.imageio.ImageIO;importjavax.servlet.ServletOutputStream;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.awt.image.BufferedImage;importjava.io.IOException;/**
 * 验证码
 *
 * @author zhoubin
 * @since 1.0.0
 */@RestControllerpublicclassCaptchaController{@AutowiredprivateDefaultKaptcha defaultKaptcha;@ApiOperation(value ="验证码")@GetMapping(value ="/captcha",produces ="image/jpeg")publicvoidcaptcha(HttpServletRequest request,HttpServletResponse response){// 定义response输出类型为image/jpeg类型
        response.setDateHeader("Expires",0);// Set standard HTTP/1.1 no-cache headers.
        response.setHeader("Cache-Control","no-store, no-cache, must-revalidate");// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
        response.addHeader("Cache-Control","post-check=0, pre-check=0");// Set standard HTTP/1.0 no-cache header.
        response.setHeader("Pragma","no-cache");// return a jpeg
        response.setContentType("image/jpeg");//-------------------生成验证码 begin --------------------------//获取验证码文本内容String text = defaultKaptcha.createText();System.out.println("验证码内容:"+text);//将验证码文本内容放入session
        request.getSession().setAttribute("captcha",text);//根据文本验证码内容创建图形验证码BufferedImage image = defaultKaptcha.createImage(text);ServletOutputStream outputStream =null;try{
            outputStream = response.getOutputStream();//输出流输出图片,格式为jpgImageIO.write(image,"jpg",outputStream);
            outputStream.flush();}catch(IOException e){
            e.printStackTrace();}finally{if(null!=outputStream){try{
                    outputStream.close();}catch(IOException e){
                    e.printStackTrace();}}}//-------------------生成验证码 end --------------------------}}

👆Controller 获取一个验证码。

publicRespBean login (@RequestBodyAdminLoginParam adminLoginParam,HttpServletRequest request){return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request);}

👆登录接口,验证用户名、密码、验证码。

🏀请注意,当两个用户同时登录的时候,后端会生成两个验证码,那么是不是我若是乱输入,输了其他用户的验证码也会生效呢?这当然是不可能的,如何判断 A 输入的验证码就是后台为 A 生成的验证码(而不是为其他用户生成的验证码)呢?其实在获取验证码的时候,并不是单纯地仅返回了一个图片,而是将验证码的内容放在了HttpServletRequest中,登陆的时候,会带着HttpServletRequest再回来,这样就可以确定该用户的验证码是多少,执行登录的时候,则会判断 request 中的验证码与用户输入的验证码是否一致,具体可以下载源码后查看 LoginService。

7.Redis 关键代码

packagecom.jim.server.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;importorg.springframework.data.redis.serializer.StringRedisSerializer;@ConfigurationpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(LettuceConnectionFactory connectionFactory){RedisTemplate<String,Object> template =newRedisTemplate<>();
        template.setKeySerializer(newStringRedisSerializer());
        template.setValueSerializer(newGenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(newStringRedisSerializer());
        template.setValueSerializer(newGenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(connectionFactory);return template;}}

👆配置 Redis

packagecom.jim.server.service.impl;importcom.baomidou.mybatisplus.core.toolkit.CollectionUtils;importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;importcom.jim.server.utils.AdminUtils;importcom.jim.server.mapper.MenuMapper;importcom.jim.server.pojo.Menu;importcom.jim.server.service.IMenuService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.ValueOperations;importorg.springframework.stereotype.Service;importjava.util.List;/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jim
 * @since 2022-05-11
 */@ServicepublicclassMenuServiceImplextendsServiceImpl<MenuMapper,Menu>implementsIMenuService{@AutowiredprivateMenuMapper menuMapper;@AutowiredprivateRedisTemplate redisTemplate;/**
     * @Author: Jim
     * @Description: 根据用户id 查询菜单列表
     * @Params:
     */@OverridepublicList<Menu>getMenuByAdminId(){Integer adminId =AdminUtils.getCurrentAdmin().getId();ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();List<Menu> menus =(List<Menu>) valueOperations.get("menu_"+adminId);if(CollectionUtils.isEmpty(menus)){
            menus = menuMapper.getMenusByAdminId(adminId);
            valueOperations.set("menu_"+adminId,menus);}return menus;}}

👆用户登录后会获取 menu 菜单,为了防止反复渲染造成数据库吞吐量较大,因此每次用户获取 menu 时会现在 Redis 中寻找

"menu_"+adminId

如果有则直接从 Redis 中获取,否则就从数据库中读取并存储在 Redis 中,存储名称依旧是

"menu_"+adminId

(得做到一致嘛,不然存储和读取不一致,你怎么存都读取不到)。

8.EasyPOI 关键代码

<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-spring-boot-starter</artifactId><version>4.1.3</version></dependency>

👆Maven 依赖

packagecom.jim.server.pojo;importcn.afterturn.easypoi.excel.annotation.Excel;importcn.afterturn.easypoi.excel.annotation.ExcelEntity;importcom.baomidou.mybatisplus.annotation.IdType;importcom.baomidou.mybatisplus.annotation.TableField;importcom.baomidou.mybatisplus.annotation.TableId;importcom.baomidou.mybatisplus.annotation.TableName;importcom.fasterxml.jackson.annotation.JsonFormat;importio.swagger.annotations.ApiModel;importio.swagger.annotations.ApiModelProperty;importlombok.Data;importlombok.EqualsAndHashCode;importlombok.experimental.Accessors;importjava.io.Serializable;importjava.time.LocalDate;/**
 * <p>
 * 
 * </p>
 *
 * @author jim
 * @since 2022-05-11
 */@Data@EqualsAndHashCode(callSuper =false)@Accessors(chain =true)@TableName("t_employee")@ApiModel(value="Employee对象", description="")publicclassEmployeeimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@ApiModelProperty(value ="员工编号")@TableId(value ="id", type =IdType.AUTO)privateInteger id;@ApiModelProperty(value ="员工姓名")@Excel(name="员工姓名")privateString name;// 为了方便阅读,这里省略了很多代码(注解都跟【员工姓名】是一样的)@ApiModelProperty(value="民族")@TableField(exist =false)@ExcelEntity(name="民族")privateNation nation;// 为了方便阅读,这里省略了很多代码(注解都跟【民族】是一样的)@ApiModelProperty(value="工资账套")@TableField(exist =false)privateSalary salary;}

👆为了方便阅读,具体注解看下方表格
注解用途使用位置备注@TableName(“t_employee”)对应哪一张数据库表类@Excel(name=“工龄”)导入导出时纵坐标的标题名字(也就是“姓名、年龄”那一些东西)成员属性@TableField(exist = false)数据库中不存在,是其他的实体类。成员属性@ExcelEntity(name=“职位”)与

@Excel

一样,但是这个用于注释数据库中没有,该 pojo 中引用了其他的 pojo 类。成员属性

@Data@NoArgsConstructor@RequiredArgsConstructor@EqualsAndHashCode(callSuper =false,of ="name")@TableName("t_nation")@ApiModel(value="Nation对象", description="")publicclassNationimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@ApiModelProperty(value ="id")@TableId(value ="id", type =IdType.AUTO)privateInteger id;@ApiModelProperty(value ="民族")@Excel(name="民族")@NonNullprivateString name;}

🏀注意:可以看到,由于Employee 数据库表中并没有Nation但是导出的时候有需要用到

Nation.name

因此,在 Employee 的成员属性 Nation 中并不是

@Excel

而是

@ExcelEntity

,然后再在 Nation 类中的 name 上写一下

@Excel

,这样执行导入导出的时候,读取到 Nation 时,就会去寻找 Nation 这个 pojo 类,然后再在 Nation 中找

@Excel

下的成员方法。

9.RabbitMQ 关键代码

消息发送端

🏀注意:这里的“消息发送”并不是指 “我给你发送一个消息”,而是“我给 RabbitMQ 消息队列发送一个消息”。

packagecom.jim.server.config;importcom.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;importcom.jim.server.pojo.MailConstants;importcom.jim.server.pojo.MailLog;importcom.jim.server.service.IMailLogService;importlombok.extern.slf4j.Slf4j;importorg.springframework.amqp.core.Binding;importorg.springframework.amqp.core.BindingBuilder;importorg.springframework.amqp.core.DirectExchange;importorg.springframework.amqp.core.Queue;importorg.springframework.amqp.rabbit.connection.CachingConnectionFactory;importorg.springframework.amqp.rabbit.core.RabbitTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/**
 * @author Jim
 * @Description rabbitmq配置
 * @createTime 2022年05月24日
 */@Slf4j@ConfigurationpublicclassRabbitMQConfig{@AutowiredprivateCachingConnectionFactory cachingConnectionFactory;@AutowiredprivateIMailLogService mailLogService;@BeanpublicRabbitTemplaterabbitTemplate(){RabbitTemplate rabbitTemplate =newRabbitTemplate(cachingConnectionFactory);/**
         * 消息确认毁掉,确认消息是否到达 borker
         * data: 消息唯一标识
         * ack: 确认结果
         * cause:失败原因
          */
        rabbitTemplate.setConfirmCallback((data,ack,cause)->{String msgId = data.getId();if(ack){
                log.info("suc1-- {}========>消息发送成功",msgId);
                mailLogService.update(newUpdateWrapper<MailLog>().set("status",1).eq("msgId",msgId));}else{
                log.error("err1-- {}========>消息发送失败",msgId);}});/**
         * 消息失败回调
         * msg: 消息主题
         * repCode:响应码
         * repText:响应文本
         * exchange:交换机
         * routingkey:路由键
         */
        rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingkey)->{
            log.error("err2-- {}========>消息发送queue时失败",msg.getBody());});return rabbitTemplate;}@BeanpublicQueuequeue(){returnnewQueue(MailConstants.MAIL_QUEUE_NAME);}@BeanpublicDirectExchangedirectExchange(){returnnewDirectExchange(MailConstants.MAIL_EXCHANGE_NAME);}@BeanpublicBindingbinding(){returnBindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);}}

👆以上内容为RabbitMQ 的配置文件

packagecom.jim.server.service.impl;importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;importcom.baomidou.mybatisplus.core.metadata.IPage;importcom.baomidou.mybatisplus.extension.plugins.pagination.Page;importcom.baomidou.mybatisplus.extension.service.impl.ServiceImpl;importcom.jim.server.mapper.EmployeeMapper;importcom.jim.server.mapper.MailLogMapper;importcom.jim.server.pojo.*;importcom.jim.server.service.IEmployeeService;importorg.springframework.amqp.rabbit.connection.CorrelationData;importorg.springframework.amqp.rabbit.core.RabbitTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importjava.text.DecimalFormat;importjava.time.LocalDate;importjava.time.LocalDateTime;importjava.time.temporal.ChronoUnit;importjava.util.List;importjava.util.Map;importjava.util.UUID;/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jim
 * @since 2022-05-11
 */@ServicepublicclassEmployeeServiceImplextendsServiceImpl<EmployeeMapper,Employee>implementsIEmployeeService{@AutowiredprivateEmployeeMapper employeeMapper;@AutowiredprivateRabbitTemplate rabbitTemplate;@AutowiredprivateMailLogMapper mailLogMapper;// 添加员工并发送邮件@OverridepublicRespBeanaddEmp(Employee employee){// 处理合同期限(保留两位小数)LocalDate beginContract = employee.getBeginContract();LocalDate endContract = employee.getEndContract();long days = beginContract.until(endContract,ChronoUnit.DAYS);DecimalFormat decimalFormat =newDecimalFormat("##.00");
        employee.setContractTerm(Double.parseDouble(decimalFormat.format(days/365.00)));if(1== employeeMapper.insert(employee)){Employee emp = employeeMapper.getEmployee(employee.getId()).get(0);// 数据库中记录消息String msgId =UUID.randomUUID().toString();MailLog mailLog =newMailLog();
            mailLog.setMsgId(msgId);
            mailLog.setEid(employee.getId());
            mailLog.setStatus(0);
            mailLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
            mailLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
            mailLog.setCount(0);
            mailLog.setTryTime(LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT));
            mailLog.setCreateTime(LocalDateTime.now());
            mailLog.setUpdateTime(LocalDateTime.now());
            mailLogMapper.insert(mailLog);// 发送信息
            rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,MailConstants.MAIL_ROUTING_KEY_NAME,emp,newCorrelationData(msgId));returnRespBean.success("添加成功");}returnRespBean.error("添加失败");}}

👆插入员工的同时,会生成一个邮件日志对象

MailLog

,并将该对象插入到数据库中,然后调用

rabbitTemplate.convertAndSend()

方法

👆仔细推敲一下这个方法的话,可以发现这并不是执行了一次‘发送’,而是执行了一次写入,这两者之间还是有些区别的;我给你“发送”一封邮件,你会收到这封邮件,但是我将一封邮件执行了一次“写入”,那么他还是在我本地的电脑中,并没有发给你。

👆。

👇以下内容包含:RabbitMQ 监听,这里是一个与 yeb-server 同级的项目,运行端口是 8082

🏀注意:这里是消息接收端,但是并不是“我接收你的消息”,而是“我接收 RabbitMQ 的消息”

packagecom.jim.mail.mail;/**
 * @author Jim
 * @Description 接收邮件
 * @createTime 2022年05月24日
 */@ComponentpublicclassMailReceiver{/**
     * author Jim
     * 端口监听
     */@RabbitListener(queues =MailConstants.MAIL_QUEUE_NAME)publicvoidhandler(Message message,Channel channel){System.out.println("Message:"+message+"\nChannel"+channel);}}

👆这个方法其实很简单,(并不是原项目中的方法,我删除了大部分关于 Java-Mail 的,放在下面的部分),当 RabbitMQ 消息队列中有了新的消息的时候,

@RabbitListener

就会监听到,注意我们在上面些消息队列的时候,用的 queue 与此处监听的 queue 一定要一致,监听到消息后获取一个 message 以及一个channel,随后输出 message 与 channel。

10.Java-Mail 关键代码

@RabbitListener(queues =MailConstants.MAIL_QUEUE_NAME)publicvoidhandler(Message message,Channel channel){Employee employee =(Employee) message.getPayload();System.out.println("MailReceiver:  employee = "+ employee);MessageHeaders headers = message.getHeaders();long tag =(long) headers.get(AmqpHeaders.DELIVERY_TAG);System.out.println("tag = "+ tag);String msgId =(String) headers.get("spring_returned_message_correlation");System.out.println("msgId = "+ msgId);HashOperations hash = redisTemplate.opsForHash();try{if(hash.entries("mail_log").containsKey(msgId)){//redis中包含key,说明消息已经被消费
                logger.info("消息已经被消费========>{}", msgId);/**
                 * 手动确认消息
                 * tag:消息序号
                 * multiple:是否多条
                 */
                channel.basicAck(tag,false);return;}MimeMessage msg = javaMailSender.createMimeMessage();MimeMessageHelper helper =newMimeMessageHelper(msg);
            helper.setFrom(mailProperties.getUsername());
            helper.setTo(employee.getEmail());
            helper.setSubject("入职邮件");
            helper.setSentDate(newDate());Context context =newContext();
            context.setVariable("name", employee.getName());
            context.setVariable("posName", employee.getPosition().getName());
            context.setVariable("joblevelName", employee.getJoblevel().getName());
            context.setVariable("departmentName", employee.getDepartment().getName());String mail = templateEngine.process("mail", context);
            helper.setText(mail,true);//发送邮件
            javaMailSender.send(msg);
            logger.info("邮件发送成功");//将消息id存入redis
            hash.put("mail_log", msgId,"OK");System.out.println("MailReceiver: redis---> msgId = "+ msgId);//手动确认消息
            channel.basicAck(tag,false);}catch(Exception e){try{
                channel.basicNack(tag,false,true);}catch(IOException ioException){//ioException.printStackTrace();
                logger.error("消息确认失败=====>{}", ioException.getMessage());}
            logger.error("MailReceiver + 邮件发送失败========{}", e.getMessage());}}}

@Scheduled 关键代码

🪵 由于我们执行发送邮件的时候,不一定可以一次性发送成功,那该怎么办呢?我就不发了?那不太合适吧;我一直尝试发送,也不太合适,因此我们使用

@Scheduled

注解,让一个方法每隔一段时间扫描一次 MailLog 数据库表,如果里面有状态为‘正在发送’的,那就重新发送一次,如果超过三次都失败了,就将状态标记为‘发送失败’。

为了简化阅读,这里将方法修改为:每隔十秒输出一次“我循环了…”,具体代码可以移步至我的 GitHub 查看。

@Scheduled(cron ="0/10 * * * * ?")publicvoidmailTask(){System.out.println("我循环了....");}

🏀 注意:要在 Main 方法的启动类上加入

@EnableScheduling

注解才可以。 👇

@SpringBootApplication@EnableSchedulingpublicclassYebApplication{publicstaticvoidmain(String[] args){SpringApplication.run(YebApplication.class,args);}}

11.JavaSecurity

packagecom.jim.server.config.security;importcom.jim.server.config.security.component.CustomFilter;importcom.jim.server.config.security.component.CustomUrlDecisionManager;importcom.jim.server.pojo.Admin;importcom.jim.server.service.IAdminService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.ObjectPostProcessor;importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.builders.WebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.web.access.intercept.FilterSecurityInterceptor;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateIAdminService adminService;@AutowiredprivateRestfulAccessDeniedHandler restfulAccessDeniedHandler;@AutowiredprivateRestAuthorizationEntryPoint restAuthorizationEntryPoint;@AutowiredprivateCustomUrlDecisionManager customUrlDecisionManager;@AutowiredprivateCustomFilter customFilter;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());}@Overridepublicvoidconfigure(WebSecurity web)throwsException{
        web.ignoring().antMatchers("/login","/logout","/css/**","/js/**","/index.html","favicon.ico","/doc.html","/webjars/**","/swagger-resources/**","/v2/api-docs/**","/captcha","/ws/**");}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 所有的请求都要求认证.anyRequest().authenticated().withObjectPostProcessor(newObjectPostProcessor<FilterSecurityInterceptor>(){@Overridepublic<OextendsFilterSecurityInterceptor>OpostProcess(O o){
                        o.setAccessDecisionManager(customUrlDecisionManager);
                        o.setSecurityMetadataSource(customFilter);return o;}}).and().headers().cacheControl();
        http.addFilterBefore(jwtAuthencationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthorizationEntryPoint);}@Override@BeanpublicUserDetailsServiceuserDetailsService(){return username ->{Admin admin = adminService.getAdminByUsername(username);if(null!=admin){
                admin.setRoles(adminService.getRoles(admin.getId()));return admin;}thrownewUsernameNotFoundException("用户名或密码不正确");};}@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@BeanpublicJwtAuthencationTokenFilterjwtAuthencationTokenFilter(){returnnewJwtAuthencationTokenFilter();}}

👆配置文件:相关内容里可以查看注解。

UserDetails userDetails = userDetailsService.loadUserByUsername(username);if(null== userDetails ||! passwordEncoder.matches(password,userDetails.getPassword())){returnRespBean.error("用户名或密码不正确");}if(!userDetails.isEnabled()){returnRespBean.error("账号被禁用,请联系管理员");}

👆以上是登录验证:通过 JavaSecurity 中的 UserDetails 来判断用户名与密码是否正确

🏀注意:PasswordEncoder 也是属于 JavaSecurity 中的一个类,主要用于加密。

12.FastDFS 关键代码

packagecom.jim.server.utils;importorg.csource.fastdfs.*;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.core.io.ClassPathResource;importorg.springframework.web.multipart.MultipartFile;importjava.io.ByteArrayInputStream;importjava.io.IOException;importjava.io.InputStream;/**
 * 文件上传工具类
 *
 * @author zhanglishen
 * @since 1.0.0
 */publicclassFastDFSUtils{privatestaticLogger logger =LoggerFactory.getLogger(FastDFSUtils.class);static{try{String filePath =newClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();ClientGlobal.init(filePath);}catch(Exception e){
            logger.error("FastDFS Client Init Fail! ",e);}}/**
     * 上传文件
     * @param file
     * @return
     */publicstaticString[]upload(MultipartFile file){String filename = file.getOriginalFilename();
        logger.info("File Name :"+ filename);long startTime =System.currentTimeMillis();String[] uploadResults =null;StorageClient storageClient =null;//获取storage客户端try{
            storageClient =getStorageClient();//上传try{
                uploadResults = storageClient.upload_file(file.getBytes(),filename.substring(filename.lastIndexOf(".")+1),null);}catch(IOException e){
                logger.error("IO Exception when uploadind the file:"+ filename, e);}}catch(Exception e){
            logger.error("Non IO Exception when uploadind the file:"+ filename, e);}
        logger.info("upload_file time used:"+(System.currentTimeMillis()- startTime)+" ms");//验证上传结果if(uploadResults ==null&& storageClient !=null){
            logger.error("upload file fail, error code:"+ storageClient.getErrorCode());}//上传成功返回groupName
        logger.info("upload file successfully!!!"+"group_name:"+ uploadResults[0]+", remoteFileName:"+" "+ uploadResults[1]);return uploadResults;}/**
     * 获取文件信息
     * @param groupName
     * @param remoteFileName
     * @return
     */publicstaticFileInfogetFileInfo(String groupName,String remoteFileName){try{StorageClient storageClient =getStorageClient();return storageClient.get_file_info(groupName,remoteFileName);}catch(IOException e){
            logger.error("IO Exception: Get File from Fast DFS failed", e);}catch(Exception e){
            logger.error("Non IO Exception: Get File from Fast DFS failed", e);}returnnull;}/**
     * 下载
     * @param groupName
     * @param remoteFileName
     * @return
     */publicstaticInputStreamdownFile(String groupName,String remoteFileName){try{StorageClient storageClient =getStorageClient();byte[] bytes = storageClient.download_file(groupName, remoteFileName);InputStream inputStream =newByteArrayInputStream(bytes);return inputStream;}catch(IOException e){
            logger.error("IO Exception: Get File from Fast DFS failed", e);}catch(Exception e){
            logger.error("Non IO Exception: Get File from Fast DFS failed", e);}returnnull;}/**
     * 删除文件
     * @param groupName
     * @param remoteFileName
     * @throws Exception
     */publicstaticvoiddeleteFile(String groupName,String remoteFileName)throwsException{StorageClient storageClient =getStorageClient();int i = storageClient.delete_file(groupName, remoteFileName);
        logger.info("delete file successfully!!!"+ i);}/**
     * 生成Storage客户端
     * @return
     */privatestaticStorageClientgetStorageClient()throwsIOException{TrackerServer trackerServer =getTrackerServer();StorageClient storageClient =newStorageClient(trackerServer,null);return storageClient;}/**
     * 生成Tracker服务器端
     * @return
     */privatestaticTrackerServergetTrackerServer()throwsIOException{TrackerClient trackerClient =newTrackerClient();TrackerServer trackerServer = trackerClient.getTrackerServer();return trackerServer;}/**
     * 获取文件路径
     * @return
     */publicstaticStringgetTrackerUrl(){TrackerClient trackerClient =newTrackerClient();TrackerServer trackerServer =null;StorageServer storageServer =null;try{
            trackerServer = trackerClient.getTrackerServer();
            storageServer = trackerClient.getStoreStorage(trackerServer);}catch(Exception e){
            e.printStackTrace();}return"http://"+storageServer.getInetSocketAddress().getHostString()+":8888/";}}

👆就是一些简单的配置,主要功能为:上传、下载、删除文件;以及一些服务于这三个功能的方法。

@ApiOperation(value="更新用户头像")@PostMapping("/admin/userface")publicRespBeanupdateAdminUserFace(MultipartFile file,Integer id,Authentication authentication){String[] filePath =FastDFSUtils.upload(file);String url =FastDFSUtils.getTrackerUrl()+filePath+"/"+filePath[1];return adminService.updateAdminUserFace(url,id,authentication);}

👆在AdminInfoController 中调用FastDFSUtils实现上传头像的功能。

13.WebSocket 关键代码

配置文件

packagecom.jim.server.config;importcom.jim.server.config.security.JwtTokenUtil;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Configuration;importorg.springframework.messaging.Message;importorg.springframework.messaging.MessageChannel;importorg.springframework.messaging.simp.config.ChannelRegistration;importorg.springframework.messaging.simp.config.MessageBrokerRegistry;importorg.springframework.messaging.simp.stomp.StompCommand;importorg.springframework.messaging.simp.stomp.StompHeaderAccessor;importorg.springframework.messaging.support.ChannelInterceptor;importorg.springframework.messaging.support.MessageHeaderAccessor;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.util.StringUtils;importorg.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;importorg.springframework.web.socket.config.annotation.StompEndpointRegistry;importorg.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;/**
 * @author Jim
 * @Description WebSocket 配置类
 * @createTime 2022年05月26日
 */@Configuration@EnableWebSocketMessageBrokerpublicclassWebSocketConfigimplementsWebSocketMessageBrokerConfigurer{@Value("${jwt.tokenHead}")privateString tokenHead;@AutowiredprivateJwtTokenUtil jwtTokenUtil;@AutowiredprivateUserDetailsService userDetailsService;/**
     * @Author: Jim
     * @Description: 添加这个 EndPoint,这样可以在网页通过 websocket 连接上服务
     * 也就是我们配置 websocket 服务复制,并且可以指定是否使用 socketJS
     * @param registry
     */@OverridepublicvoidregisterStompEndpoints(StompEndpointRegistry registry){/**
         * 1.将 ws/ep路径注册为 stomp 的端点,用户链接了这个端点就可以进行 websocket 通讯,支持 socketJS
         * 2.setAllowedOrigins("*")允许跨域
         * wethSockJS() 支持 socketJS 访问
         */
        registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();}/**
     * 输入通道参数配置
     * @param registration
     */@OverridepublicvoidconfigureClientInboundChannel(ChannelRegistration registration){
        registration.interceptors(newChannelInterceptor(){@OverridepublicMessage<?>preSend(Message<?> message,MessageChannel channel){StompHeaderAccessor accessor =MessageHeaderAccessor.getAccessor(message,StompHeaderAccessor.class);//判断是否为连接,如果是,需要获取token,并且设置用户对象if(StompCommand.CONNECT.equals(accessor.getCommand())){String token = accessor.getFirstNativeHeader("Auth-Token");if(!StringUtils.isEmpty(token)){String authToken = token.substring(tokenHead.length());String username = jwtTokenUtil.getUserNameFromToken(authToken);//token中存在用户名if(!StringUtils.isEmpty(username)){//登录UserDetails userDetails = userDetailsService.loadUserByUsername(username);//验证token是都有效if(jwtTokenUtil.validateToken(authToken,userDetails)){UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(userDetails,null, userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                                accessor.setUser(authenticationToken);}}}}return message;}});}/**
     * @Author: Jim
     * @Description: 配置消息代理
     * @param registry
     */@OverridepublicvoidconfigureMessageBroker(MessageBrokerRegistry registry){// 配置代理域,可以配置多个,配置代理的目的地前缀为/queue,可以在配置域上向客户端推送消息
        registry.enableSimpleBroker("/queue");}}

实现聊天功能

@MessageMapping("/ws/chat")publicvoidhandleMsg(Authentication authentication,ChatMsg chatMsg){Admin admin =(Admin) authentication.getPrincipal();
    chatMsg.setFrom(admin.getUsername());
    chatMsg.setFromNickName(admin.getName());
    chatMsg.setDate(LocalDateTime.now());

    simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(),"/queue/chat",chatMsg);}

👆使用simpMessagingTemplate发送消息

获取所有管理员(聊天列表)

package com.jim.server.controller;

import com.jim.server.pojo.Admin;
import com.jim.server.service.IAdminService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author Jim
 * @Description 可以跟谁聊天
 * @createTime 2022年05月26日
 */

@RestController
@RequestMapping("/chat")
public class ChatController {

    @Autowired
    private IAdminService adminService;

    @ApiOperation(value="获取所有操作员")
    @GetMapping("/admin")
    public List<Admin> getAllAdmins(String keywords){
        return adminService.getAllAdmins(keywords);
    }
}

前端跨域

由于项目前后端分离,前端必须要解决的一个问题便是跨域,否则前后端无法进行联调,因此在视频第七集的位置说明了如何实现跨域,具体方法在视频中有介绍,简单描述就是:在 Vue 项目的根目录下创建一个 vue.config.js 文件 👉🏻 在里面配置 proxy 以及 target 目标地址;

这时候仅需要将 target改成

http://49.235.72.137:8081

便可以以我的服务器为后端进行联调开发,具体实现效果如下:

实现跨域的方式还有很多种,这是开发过程中比较方便的一种。

公益开放后端,请勿随便修改服务器数据(为了方便别人访问,改了请改回来哦)

生活

放一张我胖儿的帅照:


本文转载自: https://blog.csdn.net/Hsk_03/article/details/125341293
版权归原作者 fyn.L 所有, 如有侵权,请联系我们删除。

“【代码阅读】云E办项目后端技术栈总结及源码分析”的评论:

还没有评论