项目来源: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 | 谷歌的验证码生成器 | 跳转至关键代码 |
Redis | Menu 操作时,减少数据库吞吐量。 | 跳转至关键代码 |
EasyPOI | 表格操作(导出员工信息/导入员工信息并添加到数据库) | 跳转至关键代码 |
RabbitMQ | 消息队列机制,我将需要处理的消息以队列的形式放入 RabbitMQ,随后再去读取。 | 跳转至关键代码 |
JavaMail | Java发送邮件。 | 跳转至关键代码 |
@Scheduled | 这并不是一个技术栈,但是也准备讲一下。 | 跳转至关键代码 |
SpringSecurity | Springboot的安全技术,用于加密等。 | 跳转至关键代码 |
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 效果/关键代码
🏀 具体操作可以访问-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改成后端地址便可以以我的服务器为后端进行联调开发,具体实现效果如下:
实现跨域的方式还有很多种,这是开发过程中比较方便的一种。
公益开放后端,请勿随便修改服务器数据(为了方便别人访问,改了请改回来哦)
生活
放一张我胖儿的帅照: