Bootstrap

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

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

源码地址:https://github.com/JimLearnSpace/YEB

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

  • Swagger2 接口文档:[因为一些原因,不在开放服务器接口了]

开发环境:MacBook air 2020(M1)

  • IDEA + WebStorm
  • MySQL 数据库

服务器环境:腾讯云 CentOS 8.0




👆前端截图
👆结构概述

后端技术栈

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




1.Springboot 关键代码

@SpringBootApplication
public class YebApplication {
    public static void main(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="")
public class Position implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "职位")
    @Excel(name="职位")
    @NonNull
    private String name;

    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyy-MM-dd",timezone="Asia/Shanghai")
    private LocalDateTime createDate;

    @ApiModelProperty(value = "是否启用")
    private Boolean enabled;


}

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

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

@Slf4j
public class AdminController {
  public void sout(){
    log.info("这是 info 日志信息");
    log.warn("这是 warnning 日志信息");
    log.error("这是 error 日志信息");
  }
}

👆使用 @Slf4j 生成log 日志

回到顶部




3.AutoGenerator 关键代码

package com.jim.generator;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.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_content
public class CodeGenerator {

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        final String 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 = new DataSourceConfig();
        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);

        // 包配置
        final PackageConfig pc = new PackageConfig();
        //pc.setModuleName(scanner("模块名"));
        pc.setParent("com.jim.server")
                .setEntity("pojo")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(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 = new TemplateConfig();

        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        //数据库表映射到实体的命名策略
        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(new FreemarkerTemplateEngine());
        mpg.execute();
    }


}

👆这段代码中有独立的 main 函数,可以直接运行此类,运行后在控制台输入数据库表名称,可以直接生成mapper、service、controller、pojo 类,节省无效开发的时间。(本技术使用的是 MyBatis-Plus 的代码自动生成器,具体可以去 MyBatis-Plus 官网查看)

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

回到顶部




4.Swagger2 效果/关键代码

👆Swagger 对 pojo 的展示
👆Swagger 对 Controller 接口的展示

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

// Swagger2 配置文件
package com.jim.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.jim.server.controller"))
                .paths(PathSelectors.any())
                .build()
                .securityContexts(securityContexts())
                .securitySchemes(securitySchemes());
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("云E办接口文档")
                .description("云E办接口文档")
                .contact(new Contact("Jim","http://localhost:8081/doc.html","[email protected]"))
                .version("1.0")
                .build();
    }

    private List<ApiKey> securitySchemes(){
        // 设置请求头信息
        List<ApiKey> result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization","Authorization","Header");
        result.add(apiKey);
        return result;
    }

    private List<SecurityContext> securityContexts(){
        // 设置需要登录认证的路程
        List<SecurityContext> result = new ArrayList<>();
        result.add(getContextByPath("/hello/.*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex){
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.regex(pathRegex))
                .build();
    }

    private List<SecurityReference> defaultAuth() {
        List<SecurityReference> result = new ArrayList<>();
        AuthorizationScope authorizationScope = new AuthorizationScope("global","accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        result.add(new SecurityReference("Authorization",authorizationScopes));
        return result;
    }
}
@RestController
@RequestMapping("/admin")
public class AdminController {
  
    @ApiOperation(value = "获取所有操作员")
    @GetMapping("/")
    public List<Admin> getAllAdmins(String keywords){
        return adminService.getAllAdmins(keywords);
    }
  
}

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

@ApiModel(value="Admin对象", description="")
public class Admin implements Serializable, UserDetails {
    @ApiModelProperty(value = "id")
    private Integer id;
}

👆在 pojo 类中使用@ApiModel 注解让 Swagger 知道当前内容是一个对象类,value为接口文档中显示的名称,description为接口文档中显示的简介。

👆在成员属性中用@ApiModelProperty(value = "id")注释描述属性的名字。

回到顶部




5.JWT 关键代码

package com.jim.server.config.security;


import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtTokenUtil {

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * @Author: Jim
     * @Description: 根据用户信息生成 TOKEN
     * @Params:  public
     */
    public String generateToken(UserDetails userDetails){
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,new Date());
        System.out.println(claims.toString());
        return generateToken(claims);
    }

    /**
     * @Author: Jim
     * @Description: 从 token 中获取登录用户名
     * @Params: 
     */
    public String getUserNameFromToken(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:
     */
    public boolean validateToken(String token,UserDetails userDetails){
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername() ) && ! isTokenExpired(token);
    }

    /**
     * @Author: Jim
     * @Description: 判断 token 是否可以刷新
     * @Params:
     */
    public boolean canRefresh(String token){
        return !isTokenExpired(token);
    }

    /**
     * @Author: Jim
     * @Description: 刷新 token
     * @Params:
     */
    public String refreshToken(String token){
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claims);
    }

    /**
     * @Author: Jim
     * @Description: 判断 token 是否失效
     * @Params:
     */
    private boolean isTokenExpired(String token) {
        Date expireDate = getExpiredDateFromToken(token);
        return expireDate.before(new Date());
    }

    /**
     * @Author: Jim
     * @Description: 从 token 中获取过期时间
     * @Params:
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();

    }

    /**
     * @Author: Jim
     * @Description: 从 token 中获取荷载
     * @Params:
     */
    private Claims getClaimsFromToken(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: 
     */
    private String generateToken(Map<String,Object> claims){
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * @Author: Jim
     * @Description: 生成 TOKEN 失效时间
     * @Params:
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis()+expiration*1000);
    }
}

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

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

package com.jim.server.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthencationTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader(tokenHeader);

        // 存在 token
        if(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 = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request,response);
    }
}

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


回到顶部




6.Kaptcha 关键代码

package com.jim.server.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * 验证码配置类
 * @author Jim
 * @since 1.0.0
 */
@Configuration
public class CaptchaConfig {

    @Bean
    public DefaultKaptcha defaultKaptcha(){
        //验证码生成器
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        //配置
        Properties properties = new Properties();
        //是否有边框
        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 = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}

👆以上为 Kaptcha 的配置类,主要操作为创建一个DefaultKaptcha对象,并对对象进行配置。

package com.jim.server.controller;

import com.google.code.kaptcha.impl.DefaultKaptcha;
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.RestController;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
 * 验证码
 *
 * @author zhoubin
 * @since 1.0.0
 */
@RestController
public class CaptchaController {

    @Autowired
    private DefaultKaptcha defaultKaptcha;

    @ApiOperation(value = "验证码")
    @GetMapping(value = "/captcha",produces = "image/jpeg")
    public void captcha(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();
            //输出流输出图片,格式为jpg
            ImageIO.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 获取一个验证码。

public RespBean login (@RequestBody AdminLoginParam adminLoginParam, HttpServletRequest request){
    return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),adminLoginParam.getCode(),request);
}

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

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

回到顶部




7.Redis 关键代码

package com.jim.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(connectionFactory);
        return template;
    }
}

👆配置 Redis

package com.jim.server.service.impl;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jim.server.utils.AdminUtils;
import com.jim.server.mapper.MenuMapper;
import com.jim.server.pojo.Menu;
import com.jim.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jim
 * @since 2022-05-11
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IMenuService {


    @Autowired
    private MenuMapper menuMapper;

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * @Author: Jim
     * @Description: 根据用户id 查询菜单列表
     * @Params:
     */
    @Override
    public List<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 依赖

package com.jim.server.pojo;

import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDate;

/**
 * <p>
 * 
 * </p>
 *
 * @author jim
 * @since 2022-05-11
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_employee")
@ApiModel(value="Employee对象", description="")
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "员工编号")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "员工姓名")
    @Excel(name="员工姓名")
    private String name;

  	// 为了方便阅读,这里省略了很多代码(注解都跟【员工姓名】是一样的)

   
    @ApiModelProperty(value="民族")
    @TableField(exist = false)
    @ExcelEntity(name="民族")
    private Nation nation;
  
  	// 为了方便阅读,这里省略了很多代码(注解都跟【民族】是一样的)

    @ApiModelProperty(value="工资账套")
    @TableField(exist = false)
    private Salary 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="")
public class Nation implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @ApiModelProperty(value = "民族")
    @Excel(name="民族")
    @NonNull
    private String name;
}

🏀注意:可以看到,由于Employee 数据库表中并没有Nation但是导出的时候有需要用到 Nation.name因此,在 Employee 的成员属性 Nation 中并不是@Excel而是@ExcelEntity,然后再在 Nation 类中的 name 上写一下@Excel,这样执行导入导出的时候,读取到 Nation 时,就会去寻找 Nation 这个 pojo 类,然后再在 Nation 中找@Excel下的成员方法。

回到顶部




9.RabbitMQ 关键代码

消息发送端

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

package com.jim.server.config;

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.jim.server.pojo.MailConstants;
import com.jim.server.pojo.MailLog;
import com.jim.server.service.IMailLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Jim
 * @Description rabbitmq配置
 * @createTime 2022年05月24日
 */
@Slf4j
@Configuration
public class RabbitMQConfig {

    @Autowired
    private CachingConnectionFactory cachingConnectionFactory;
    @Autowired
    private IMailLogService mailLogService;
    @Bean
    public RabbitTemplate rabbitTemplate(){
        RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);

        /**
         * 消息确认毁掉,确认消息是否到达 borker
         * data: 消息唯一标识
         * ack: 确认结果
         * cause:失败原因
          */
        rabbitTemplate.setConfirmCallback((data,ack,cause)->{
            String msgId = data.getId();
            if(ack){
                log.info("suc1-- {}========>消息发送成功",msgId);
                mailLogService.update(new UpdateWrapper<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;
    }



    @Bean
    public Queue queue(){
        return new Queue(MailConstants.MAIL_QUEUE_NAME);
    }

    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME);
    }

    @Bean
    public Binding binding(){
        return BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
    }
}

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

package com.jim.server.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jim.server.mapper.EmployeeMapper;
import com.jim.server.mapper.MailLogMapper;
import com.jim.server.pojo.*;
import com.jim.server.service.IEmployeeService;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jim
 * @since 2022-05-11
 */
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {


    @Autowired
    private EmployeeMapper employeeMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private MailLogMapper mailLogMapper;

  	// 添加员工并发送邮件
    @Override
    public RespBean addEmp(Employee employee) {
        // 处理合同期限(保留两位小数)
        LocalDate beginContract = employee.getBeginContract();
        LocalDate endContract = employee.getEndContract();
        long days = beginContract.until(endContract, ChronoUnit.DAYS);

        DecimalFormat decimalFormat = new DecimalFormat("##.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 = new MailLog();
            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,new CorrelationData(msgId));
            return RespBean.success("添加成功");
        }
        return RespBean.error("添加失败");
    }
}

👆插入员工的同时,会生成一个邮件日志对象MailLog,并将该对象插入到数据库中,然后调用rabbitTemplate.convertAndSend()方法

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

👆至于这个邮件写入到了哪里了呢?那当然是 RabbitMQ 消息队列,关于 RabbitMQ 消息队列的详细信息可以查看官网,下载安装可以查看该博客。由 RabbitMQ 消息队列收到了这封邮件的信息之后,消息会存储在消息队列中,随后通过其他的项目去监听 RabbitMQ 中的内容,只要消息队列里面有了新的消息,那么我就执行一次发送消息(这里使用的是Java-Mail

回到顶部

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

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

package com.jim.mail.mail;

/**
 * @author Jim
 * @Description 接收邮件
 * @createTime 2022年05月24日
 */
@Component
public class MailReceiver {
    /**
     * author Jim
     * 端口监听
     */
    @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
    public void handler(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)
    public void handler(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 = new MimeMessageHelper(msg);
            helper.setFrom(mailProperties.getUsername());
            helper.setTo(employee.getEmail());
            helper.setSubject("入职邮件");
            helper.setSentDate(new Date());
            Context context = new Context();
            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 * * * * ?")
public void mailTask(){
   System.out.println("我循环了....");
}

🏀 注意:要在 Main 方法的启动类上加入@EnableScheduling注解才可以。 👇

@SpringBootApplication
@EnableScheduling
public class YebApplication {
    public static void main(String[] args){
        SpringApplication.run(YebApplication.class,args);
    }
}

回到顶部




11.JavaSecurity

package com.jim.server.config.security;

import com.jim.server.config.security.component.CustomFilter;
import com.jim.server.config.security.component.CustomUrlDecisionManager;
import com.jim.server.pojo.Admin;
import com.jim.server.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IAdminService adminService;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
    @Autowired
    private CustomUrlDecisionManager customUrlDecisionManager;
    @Autowired
    private CustomFilter customFilter;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/login",
                "/logout",
                "/css/**",
                "/js/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/captcha",
                "/ws/**"
        );
    }

    @Override
    protected void configure(HttpSecurity http) throws  Exception{
        http.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 所有的请求都要求认证
                .anyRequest()
                .authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>(){
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(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
    @Bean
    public UserDetailsService userDetailsService(){
        return username ->{
            Admin admin = adminService.getAdminByUsername(username);
            if(null!=admin){
                admin.setRoles(adminService.getRoles(admin.getId()));
                return admin;
            }
            throw new UsernameNotFoundException("用户名或密码不正确");
        };
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){
        return new JwtAuthencationTokenFilter();
    }
}

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

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

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

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

回到顶部




12.FastDFS 关键代码

package com.jim.server.utils;

import org.csource.fastdfs.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * 文件上传工具类
 *
 * @author zhanglishen
 * @since 1.0.0
 */

public class FastDFSUtils {
    private static Logger logger = LoggerFactory.getLogger(FastDFSUtils.class);

    static {
        try {
            String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
            ClientGlobal.init(filePath);
        } catch (Exception e) {
            logger.error("FastDFS Client Init Fail! ",e);
        }
    }

    /**
     * 上传文件
     * @param file
     * @return
     */
    public static String[] 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
     */
    public static FileInfo getFileInfo(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);
        }
        return null;
    }

    /**
     * 下载
     * @param groupName
     * @param remoteFileName
     * @return
     */
    public static InputStream downFile(String groupName,String remoteFileName){
        try {
            StorageClient storageClient = getStorageClient();
            byte[] bytes = storageClient.download_file(groupName, remoteFileName);
            InputStream inputStream = new ByteArrayInputStream(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);
        }
        return null;
    }

    /**
     * 删除文件
     * @param groupName
     * @param remoteFileName
     * @throws Exception
     */
    public static void deleteFile(String groupName,String remoteFileName) throws Exception {
        StorageClient storageClient = getStorageClient();
        int i = storageClient.delete_file(groupName, remoteFileName);
        logger.info("delete file successfully!!!" + i);
    }

    /**
     * 生成Storage客户端
     * @return
     */
    private static StorageClient getStorageClient() throws IOException {
        TrackerServer trackerServer = getTrackerServer();
        StorageClient storageClient = new StorageClient(trackerServer, null);
        return storageClient;
    }

    /**
     * 生成Tracker服务器端
     * @return
     */
    private static TrackerServer getTrackerServer() throws IOException {
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getTrackerServer();
        return trackerServer;
    }

    /**
     * 获取文件路径
     * @return
     */
    public static String getTrackerUrl(){
        TrackerClient trackerClient = new TrackerClient();
        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")
public RespBean updateAdminUserFace(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 关键代码

配置文件

package com.jim.server.config;

import com.jim.server.config.security.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author Jim
 * @Description WebSocket 配置类
 * @createTime 2022年05月26日
 */

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * @Author: Jim
     * @Description: 添加这个 EndPoint,这样可以在网页通过 websocket 连接上服务
     * 也就是我们配置 websocket 服务复制,并且可以指定是否使用 socketJS
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        /**
         * 1.将 ws/ep路径注册为 stomp 的端点,用户链接了这个端点就可以进行 websocket 通讯,支持 socketJS
         * 2.setAllowedOrigins("*")允许跨域
         * wethSockJS() 支持 socketJS 访问
         */
        registry.addEndpoint("/ws/ep").setAllowedOrigins("*").withSockJS();
    }

    /**
     * 输入通道参数配置
     * @param registration
     */
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> 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 = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                                accessor.setUser(authenticationToken);
                            }
                        }
                    }
                }
                return message;
            }
        });
    }

    /**
     * @Author: Jim
     * @Description: 配置消息代理
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 配置代理域,可以配置多个,配置代理的目的地前缀为/queue,可以在配置域上向客户端推送消息
        registry.enableSimpleBroker("/queue");
    }
}

实现聊天功能

@MessageMapping("/ws/chat")
public void handleMsg(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改成后端地址便可以以我的服务器为后端进行联调开发,具体实现效果如下:

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

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

回到顶部

生活

放一张我胖儿的帅照:

回到顶部

;