irpas技术客

【代码阅读】云E办项目后端技术栈总结及源码分析_不食花生的猫_云e办源码

未知 6561

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

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

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

Swagger2 接口文档:http://110.40.191.51:8081/doc.html

只想学前端的朋友可以将自己的前端跨域地址设置为http://110.40.191.51:8081,具体方法点击此处

开发环境:MacBook air 2020

IDEA + WebStormMySQL 数据库

服务器环境:腾讯云 CentOS 8.0

个人博客地址:Jime.cc

👆前端截图 👆结构概述 后端技术栈 技术名称作用关键代码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","1234@qq.com")) .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改成 http://110.40.191.51:8081便可以以我的服务器为后端进行联调开发,具体实现效果如下:

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

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

回到顶部

生活

放一张我胖儿的帅照:

回到顶部


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #云e办源码 #前一段时间跟着 #b #站手敲了一下云 #e #办项目今天写一下技术总结