This article records the whole back-end development process of the project
MybatisPlus code generator
A separate Maven module was used in this project
<dependencies> <!--web rely on--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis-plus rely on--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1.tmp</version> </dependency> <!--mybatis-plus Code generator dependency--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.1.tmp</version> </dependency> <!--freemarker rely on--> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <!--mysql rely on--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>
codeGenerator configuration class
package com.zlq.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.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.FileOutConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.PackageConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.TemplateConfig; 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; public class CodeGenerator { /** * <p> * Read console content * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("Please enter" + tip + ": "); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("Please enter the correct" + tip + "!"); } public static void main(String[] args) { // Code generator AutoGenerator mpg = new AutoGenerator(); // Global configuration GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/yeb-generator/src/main/java"); //author gc.setAuthor("liqun"); //Open output directory gc.setOpen(false); //xml to open BaseResultMap gc.setBaseResultMap(true); //xml open BaseColumnList gc.setBaseColumnList(true); // Entity attribute Swagger2 annotation gc.setSwagger2(true); mpg.setGlobalConfig(gc); // Data source configuration DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/yeb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia" + "/Shanghai"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("zaxscdvf"); mpg.setDataSource(dsc); // Package configuration PackageConfig pc = new PackageConfig(); pc.setParent("com.zlq") .setEntity("pojo") .setMapper("mapper") .setService("service") .setServiceImpl("service.impl") .setController("controller"); mpg.setPackageInfo(pc); // Custom configuration InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // If the template engine is freemaker String templatePath = "/templates/mapper.xml.ftl"; // If the template engine is velocity // String templatePath = "/templates/mapper.xml.vm"; // Custom output configuration List<FileOutConfig> focList = new ArrayList<>(); // Custom configuration will be output first focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // Customize the output file name. If you set the Prefix suffix for Entity, note that the name of xml will change!! return projectPath + "/yeb-generator/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // Configuration template TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // Policy configuration StrategyConfig strategy = new StrategyConfig(); //Named policy mapping entities to database tables strategy.setNaming(NamingStrategy.underline_to_camel); //Naming policy for mapping database table fields to entities strategy.setColumnNaming(NamingStrategy.no_change); //lombok model strategy.setEntityLombokModel(true); //Generate @ RestController controller strategy.setRestControllerStyle(true); strategy.setInclude(scanner("Table name, separated by multiple English commas").split(",")); strategy.setControllerMappingHyphenStyle(true); //Table prefix strategy.setTablePrefix("t_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }
Implementation of login function based on spring security
1. Configuration of spring security
- Add dependency
<!--security rely on--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--JWT rely on--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
- yaml configuration
jwt: # Request header stored by JWT tokenHeader: Authorization # Key used for JWT decryption secret: yeb-secret # Overdue time of JWT (60 * 60 * 24 * 7) expiration: 604800 # Get the start in JWT load tokenHead: Bearer
-
JwtToken tool class
package com.zlq.server.config.security.component; 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; /** * @ProjectName:yeb * @Package:com.zlq.server.config.security * @ClassName: JwtTokenUtil * @description: * @author: LiQun * @CreateDate:2021/4/23 10:36 afternoon */ @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; /** * Function Description: < br > * 〈Generate token based on user information * * @param userDetails * @Author: Larry * @Date: 2021/4/23 10:40 afternoon * @return: java.lang.String */ 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()); return generateToken(claims); } /** * Function Description: < br > * 〈Generate JWT according to load, token > * * @param claims * @Author: Larry * @Date: 2021/4/23 11:08 afternoon * @return: java.lang.String */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * Function Description: < br > * 〈Get username from token * * @param token * @Author: Larry * @Date: 2021/4/23 11:11 afternoon * @return: java.lang.String */ public String getUsernameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * Function Description: < br > * 〈Generate token expiration time * * @param * @Author: Larry * @Date: 2021/4/23 11:08 afternoon * @return: java.util.Date */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * Function Description: < br > * 〈Get load from token * * @param token * @Author: Larry * @Date: 2021/4/23 11:17 afternoon * @return: io.jsonwebtoken.Claims */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws( token.replaceFirst("\ufeff", "")) .getBody(); } catch (Exception e) { e.printStackTrace(); } return claims; } /** * Function Description: < br > * 〈Verify whether the token is valid * * @param token * @param userDetails * @Author: Larry * @Date: 2021/4/23 11:21 afternoon * @return: boolean */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUsernameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * Function Description: < br > * 〈Judge whether the token can be refreshed * @Author: Larry * @Date: 2021/4/23 11:27 afternoon * @param token * @return: boolean */ public boolean canRefresh(String token){ return !isTokenExpired(token); } /** * Function Description: < br > * 〈Refresh token * @Author: Larry * @Date: 2021/4/23 11:30 afternoon * @param token * @return: java.lang.String */ public String refreshToken(String token){ Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED,new Date()); return generateToken(claims); } /** * Function Description: < br > * 〈Determine whether the token is invalid * * @param token * @Author: Larry * @Date: 2021/4/23 11:21 afternoon * @return: boolean */ private boolean isTokenExpired(String token) { Date expireDate = getExpiredDateFromToken(token); return expireDate.before(new Date()); } /** * Function Description: < br > * 〈Get expiration time from token * * @param token * @Author: Larry * @Date: 2021/4/23 11:24 afternoon * @return: java.util.Date */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } }
-
SpringSecurity configuration class
package com.zlq.server.config.security; import com.zlq.server.config.security.component.*; import com.zlq.server.pojo.Admin; import com.zlq.server.service.IAdminService; import com.zlq.server.service.IRoleService; 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.UserDetails; 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; import javax.annotation.Resource; @Configuration public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Resource private IAdminService adminService; @Resource private RestFulAccessDeniedHandler restFulAccessDeniedHandler; @Resource private RestAuthorizationEntryPoint restAuthorizationEntryPoint; @Resource private IRoleService roleService; @Resource private CustomUrlDecisionManager customUrlDecisionManager; @Resource private CustomFilter customFilter; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { //csrf is not required to use JWT http.csrf().disable() //Based on token, session is not required .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() //All require certification .anyRequest().authenticated() .withObjectPostProcessor( new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setAccessDecisionManager(customUrlDecisionManager); object.setSecurityMetadataSource(customFilter); return object; } } ) .and() //disable cache .headers() .cacheControl(); //Add JWT login authorization filter http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //Add custom unauthorized and unregistered result return http.exceptionHandling() .accessDeniedHandler(restFulAccessDeniedHandler) .authenticationEntryPoint(restAuthorizationEntryPoint); } @Override public void configure(WebSecurity web) throws Exception { //Release static resources web.ignoring().antMatchers( "/login", "/logout", "/ws/**", "/css/**", "/js/**", "/index.html", "/favicon.ico", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs/**", "/kaptcha"); } /*Because it inherits WebSecurityConfigurerAdapter, Moreover, there is a userDetailsService() method in the WebSecurityConfigurerAdapter class So it can be written like this. The - > in it represents loadUserByUsername in the userDetailsService interface. In fact, the following methods can also be written separately in a class */ @Bean @Override public UserDetailsService userDetailsService() { return username -> { Admin admin = adminService.getAdminByUsername(username); if (null != admin) { admin.setRoles(roleService.getRoleListByUserId(admin.getId())); return admin; } throw new UsernameNotFoundException("Incorrect user name or password!"); }; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() { return new JwtAuthenticationTokenFilter(); } }
2. Implement UserDetails class based on Admin entity class
Our Admin entity class is based on t_ The Admin table is generated through MybatisPlusGenerate. The fields contained in the table are as follows. To use it in combination with spring security, it must implement the UserDetails class. The UserDetails class stores user information, which is encapsulated in the Authentication object. This allows you to store non security related user information (e.g. e-mail address, phone number, etc.) in your own class.
3. Implementation of login function based on spring security
-
Create LoginParams class
We only need three parameters: user name, password and verification code for login verification, so we can create an AdminLoginParams, which only needs three parameters: user name and password verification code
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) //Accessors supports chain programming @ApiModel(value = "AdminLoginParam object", description = "User name and password specially used to pass to the front end") public class AdminLoginParams { @ApiModelProperty(value = "user name", required = true) private String username; @ApiModelProperty(value = "password", required = true) private String password; @ApiModelProperty(value = "Verification Code", required = true) private String code; }
-
LoginController class
@Api(tags = "LoginController") @RestController public class LoginController { @Resource private IAdminService adminService; @Resource private IRoleService roleService; @ApiOperation(value = "Return after login token") @PostMapping("/login") public RespBean login(@RequestBody AdminLoginParams adminLoginParams, HttpServletRequest request){ return adminService.login(adminLoginParams.getUsername(),adminLoginParams.getPassword(),adminLoginParams.getCode(),request); } @ApiOperation("Log out") @PostMapping("/logout") public RespBean logout(){ return RespBean.success("Logout succeeded!"); } @ApiOperation("Get current user login information") @GetMapping("/admin/info") public Admin getAdminInfo(Principal principal){ if (null == principal){ return null; } String username = principal.getName(); //Query admin information from database Admin admin= adminService.getAdminByUsername(username); //To be on the safe side, set password to null admin.setPassword(null); admin.setRoles(roleService.getRoleListByUserId(admin.getId())); return admin; } }
-
Login logic in service
@Override public RespBean login(String username, String password, String code, HttpServletRequest request) { String captcha = (String) request.getSession().getAttribute("captcha"); if (null == captcha || !captcha.equalsIgnoreCase(code)) { return RespBean.error("Verification code error, please re-enter!"); } //Login userdetails userdetails = userdetailsservice loadUserByUsername(username); If (null = = userdetails |! Passwordencoder. Matches (password, userdetails. Getpassword()) {return respbean. Error ("wrong username or password!");} If (! Userdetails. Isenabled()) {return respbean. Error ("account has been disabled, please contact the administrator");}// Update the security login user object UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())// Generate a token and put it into the result set string token = jwttokenutil generateToken(userDetails); Map<String, Object> tokenMap = new HashMap<>(); tokenMap. put("token", token); tokenMap. put("tokenHead", tokenHead); return RespBean. Success ("login succeeded", tokenMap);}
-
Add unlisted and unauthorized result return
- The result of not logging in is returned
package com.zlq.server.config.security.component;import com.fasterxml.jackson.databind.ObjectMapper;import com.zlq.server.common.RespBean;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.stereotype.Component;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;/** * @ProjectName:yeb * @Package:com.zlq.server.config.security * @ClassName: RestAuthorizationEntryPoint * @description: Customized return result of the access interface when the user is not logged in or the token is invalid * @ author: LiQun * @CreateDate:2021/4/24 10:52 PM */@Componentpublic class RestAuthorizationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); PrintWriter writer = httpServletResponse.getWriter(); RespBean respBean= RespBean.error("Not logged in yet, please log in!"); respBean.setCode(401); writer.write(new ObjectMapper().writeValueAsString(respBean)); writer.flush(); writer.close(); }}
-
Unauthorized result return
package com.zlq.server.config.security.component; import com.fasterxml.jackson.databind.ObjectMapper; import com.zlq.server.common.RespBean; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @ProjectName:yeb * @Package:com.zlq.server.config.security * @ClassName: RestFulAccessDeniedHandler * @description: When the access interface does not have permission, the user-defined returned results * @author: LiQun * @CreateDate:2021/4/24 11:07 afternoon */ @Component public class RestFulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); PrintWriter writer = httpServletResponse.getWriter(); RespBean respBean = RespBean.error("No permission!"); respBean.setCode(403); writer.write(new ObjectMapper().writeValueAsString(respBean)); writer.flush(); writer.close(); } }
-
JWT login authorization filter
package com.zlq.server.config.security.component; 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.annotation.Resource; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @ProjectName:yeb * @Package:com.zlq.server.config.security * @ClassName: JwtAuthenticationTokenFilter * @description: jwt Login authorization filter * @author: LiQun * @CreateDate:2021/4/24 10:15 afternoon */ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Resource private JwtTokenUtil jwtTokenUtil; @Resource private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { String authHeader = httpServletRequest.getHeader(tokenHeader); //Judge whether the token exists if (null != authHeader && authHeader.startsWith(tokenHead)) { String authToken = authHeader.substring(tokenHead.length()); String username = jwtTokenUtil.getUsernameFromToken(authToken); //token exists but is not logged in if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) { //Sign in UserDetails userDetails = userDetailsService.loadUserByUsername(username); //Verify whether the token is valid and reset the user object if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(httpServletRequest,httpServletResponse); } }
Integrate Swagger
-
POM dependency
<!-- swagger2 rely on --><dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version></dependency><!-- Swagger Third party ui rely on --><dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version></dependency>
-
Swagger configuration class
package com.zlq.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;/** * @ProjectName:yeb * @Package:com.zlq.server.config * @ClassName: Swagger2Config * @description: * @author: LiQun * @CreateDate:2021/4/25 6:35 afternoon */@Configuration@EnableSwagger2public class Swagger2Config { /** * Function Description: < br > * "set document information" * @ Author: Larry * @ date: 2021 / 4 / 25 7:34 PM * @ param * @ return: springfox documentation. service. ApiInfo */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("cloud E Office interface document") .description("cloud E Office interface document") .contact(new Contact("Yu Zexi", "http:localhost:8081/doc.html", "804492004@qq.com")) .version("1.0") .build(); } /** * Function Description: < br > * "create api interface swagger document" * @ Author: Larry * @ date: 2021 / 4 / 25 7:34 PM * @ param * @ return: springfox documentation. spring. web. plugins. Docket */ @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() //Scan the controllers under which packages to generate api documents apis(RequestHandlerSelectors.basePackage("com.zlq.server.controller")) .paths(PathSelectors.any()) .build() / / add login authentication securityContexts(securityContexts()) .securitySchemes(securitySchemes()); } /** * Function Description: < br > * "used to set request header information" * @ Author: Larry * @ date: 2021 / 4 / 25 7:41 PM * @ param * @ return: Java util. List<? extends springfox. documentation. service. SecurityScheme> */ private List<? Extensions securityscheme > securityschemes() {list < apikey > result = new ArrayList < > (); / / Name: name of apikey, keyname: name of key to be prepared, apikey = new apikey ("authorization", "authorization", "header"); result. Add (apikey); return result;} / * ** Function Description: < br > * "set the path that requires login authentication" * @ Author: Larry * @ date: 2021 / 4 / 25 7:45 PM * @ param * @ return: Java util. List<springfox. documentation. spi. service. contexts. SecurityContext> */ private List<SecurityContext> securityContexts() { List<SecurityContext> result = new ArrayList<>(); result.add(getContextByPath("/test/.*")); 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; }}
Note: don't forget to release Swagger in the spring sequrity configuration class
Integrate google Kaptcha verification code
-
Import dependency
<dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version></dependency>
-
Verification code configuration class
package com.zlq.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;@Configurationpublic class CaptchaConfig { @Bean public DefaultKaptcha getDefaultKaptcha() { //Verification code generator DefaultKaptcha defaultKaptcha = new DefaultKaptcha()// Configure properties = new properties()// Whether there are border properties setProperty("kaptcha.border", "yes"); // Set border color properties setProperty("kaptcha.border.color", "105,179,90"); // Border thickness. The default value is 1 / / properties setProperty("kaptcha.border.thickness","1"); // Verification code properties setProperty("kaptcha.session.key", "code"); // The character color of verification code text is black by default setProperty("kaptcha.textproducer.font.color", "blue"); // Set font style properties Setproperty ("kaptcha. Textproducer. Font. Names", "Arial, italics, Microsoft YaHei")// Font size, default 40 properties setProperty("kaptcha.textproducer.font.size", "30"); // The content range of verification code text characters is abced2345678gfynmnpwx / / properties setProperty("kaptcha.textproducer.char.string", ""); // Character length, default to 5 properties setProperty("kaptcha.textproducer.char.length", "4"); // The default character spacing is 2 properties setProperty("kaptcha.textproducer.char.space", "4"); // The width of verification code image is 200 properties by default setProperty("kaptcha.image.width", "100"); // The height of the verification code image is 40 properties by default setProperty("kaptcha.image.height", "40"); Config config = new Config(properties); defaultKaptcha. setConfig(config); return defaultKaptcha; }}
-
Interface for generating verification code
package com.zlq.server.controller;import com.google.code.kaptcha.impl.DefaultKaptcha;import io.swagger.annotations.ApiOperation;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;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;/** * @ProjectName:yeb * @Package:com.zlq.server.controller * @ClassName: CaptchaController * @description: * @author: LiQun * @CreateDate:2021/4/25 9:09 afternoon */@RestControllerpublic class CaptchaController { @Resource private DefaultKaptcha defaultKaptcha; @ApiOperation(value = "Verification Code") @GetMapping(value = "/kaptcha", produces = "image/jpeg") public void getCaptcha(HttpServletRequest request, HttpServletResponse response) { // Define the response output type as image / jpeg setDateHeader("Expires", 0); // Set standard HTTP/1.1 no-cache headers. response. setHeader("Cache-Control", "no-store, no-cache, mustrevalidate"); // 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"); //------------------- Generate verification code begin ------------------------ / / get the text content of the verification code string text = defaultkaptcha createText(); // System.out.println("verification code content:" + text)// Put the verification code into session request getSession(). setAttribute("captcha", text); // Create graphic verification code bufferedimage image = defaultkaptcha according to the text content createImage(text); ServletOutputStream outputStream = null; Try {OutputStream = response. Getoutputstream(); / / output stream output picture, format 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(); } } } //------------------- Generate verification code end -------------------------}}
Note: the verification code interface should be released in the spring security configuration class
Menu function
There are only two levels of menus in our cloud e office. We need to add a List to the menu entity class
children fieldFirst, we need to query the menu list through the user id. we can optimize the loading speed of the menu through redis. Generally speaking, the menu will not change and can be placed in the redis cache
<!-- spring data redis rely on --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- commons-pool2 Object pool dependency --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
# Redis to configure redis: timeout: 10000ms # Connection timeout host: 47.93.242.4 # Redis server address port: 6379 # Redis Server port database: 0 # Which library to select, default 0 Library password: zaxscdvf lettuce: pool: max-active: 1024 # Maximum number of connections, 8 by default max-wait: 10000ms # Maximum connection blocking wait time, in milliseconds, default -1 max-idle: 200 # Maximum idle connection, default 8 min-idle: 5 # Minimum idle connection, default 0
Configure redis
package com.zlq.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;/** * @ProjectName:yeb * @Package:com.zlq.server.config * @ClassName: RedisConfig * @description: Redis Configuration class * @ author: LiQun * @CreateDate:2021/4/26 11:19 am */@Configurationpublic class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); //Set the sequencer redistemplate for string type key setKeySerializer(new StringRedisSerializer()); // Set the sequencer redistemplate for string type value setValueSerializer(new GenericJackson2JsonRedisSerializer()); // Set the sequencer redistemplate for the hash type key setHashKeySerializer(new StringRedisSerializer()); // Set the sequencer redistemplate for hash type value setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate. setConnectionFactory(redisConnectionFactory); return redisTemplate; }}
Save the menu list in redis
@Override public List<Menu> getMenuByAdminId() { //Get user ID object principal = securitycontextholder getContext(). getAuthentication(). getPrincipal(); Integer adminId = ((Admin) principal). getId(); ValueOperations<String, Object> valueOperations = redisTemplate. opsForValue(); List<Menu> menuListInRedis = (List<Menu>) valueOperations. get("menu_" + adminId); // Judge whether there is a menuList in redis. If there is one, get it from redis. If not, check it from mysql database and put it into redis database. Then check if (collectionutils. Isempty (menulistinredis)) {list < Menu > menulistfrommysql = menumapper. Getmenubyadmid (adminid); valueoperations. Set ("menu"+ adminId, menuListFromMySQL); return (List<Menu>) valueOperations. get("menu_" + adminId); } return menuListInRedis; }
Authority management
RBAC is Role-Based Access Control. In RBAC, permissions are associated with roles, and roles are associated with users. Therefore, users can decide which resources they can operate by giving them roles
-
Add the role list attribute List roles in the menu;
-
Gets a list of all menus for the role
-
Add a filter to get the required role according to the url
package com.zlq.server.config.security.component;import com.zlq.server.pojo.Menu;import com.zlq.server.pojo.Role;import com.zlq.server.service.IMenuService;import org.springframework.security.access.ConfigAttribute;import org.springframework.security.access.SecurityConfig;import org.springframework.security.config.http.FilterInvocationSecurityMetadataSourceParser;import org.springframework.security.web.FilterInvocation;import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import javax.annotation.Resource;import java.util.Collection;import java.util.List;/** * @ProjectName:yeb * @Package:com.zlq.server.config.security.component * @ClassName: CustomFilter * @description: Analyze which roles can make requests according to the requested url * @ author: LiQun * @CreateDate:2021/4/26 2:27 PM */@Componentpublic class CustomFilter implements FilterInvocationSecurityMetadataSource { @Resource private IMenuService menuService; @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { //Get the url of the request string requesturl = ((filterinvocation) o) getRequestUrl(); // Get all menus list < Menu > menulist = menuservice getAllMenusByRoLe(); // Judge whether the requested url matches the url of the role. For (menu menu: menulist) {if (New antpathmatcher(). Match (menu. Geturl(), requesturl)) {/ / put (one or more) in this menu Save the name in the role object to the array / * turn the roles set in the menu into a stream, cycle through each element in the stream (role object), convert the stream into the name of the role object, and convert it into string [] array * / String [] STR = menu getRoles(). stream(). map(Role::getName). toArray(String[]::new); return SecurityConfig. createList(str); } } // If there is no matching url, the default is to log in and access return securityconfig createList("ROLE_LOGIN"); } @ Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @ Override public boolean supports(Class<?> aClass) { return false; }}
-
Add permission control filter to judge user role
package com.zlq.server.config.security.component;import org.springframework.security.access.AccessDecisionManager;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.access.ConfigAttribute;import org.springframework.security.authentication.AnonymousAuthenticationToken;import org.springframework.security.authentication.InsufficientAuthenticationException;import org.springframework.security.core.Authentication;import org.springframework.security.core.GrantedAuthority;import org.springframework.stereotype.Component;import java.util.Collection;/** * @ProjectName:yeb * @Package:com.zlq.server.config.security.component * @ClassName: CustomUrlDecisionManager * @description: //Permission control, judge user role * @ author: LiQun * @CreateDate:2021/4/26 4:08 PM */@Componentpublic class CustomUrlDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { for (ConfigAttribute configAttribute : configAttributes) { //Required role of current url string needrole = configattribute getAttribute(); // Judge whether the role is accessible by login. Set if ("ROLE_LOGIN".equals(needRole)) {/ / judge whether to login if (authentication instance of anonymous authentication token) {throw new accessdeniedexception ("login, please!");} else { return; } } // Determine whether the user role is the role required by the url collection <? extends GrantedAuthority> authorities = authentication. getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } Throw new accessdeniedexception ("insufficient permission, please contact the administrator!");}@ Override public boolean supports(ConfigAttribute attribute) { return true; } @ Override public boolean supports(Class<?> clazz) { return true; }}
Summarize the core of permission control: judge which roles the url exists in according to the requested url, and then judge which roles the logged in user has, so you can match which operable URLs the user contains. When you access this url, if you match the url that the user can operate, you are allowed to access. If you can't match, you will report insufficient permission
stored procedure
Today, I came across the knowledge point of using stored procedures when I was working on a project. I found that I didn't listen to this knowledge point when I was learning the basics of mysql and didn't digest it well. Therefore, I specially turned it out to learn mysql stored procedures again
MySQL version 5.0 supports stored procedures.
1. Introduction to stored procedures
Stored procedure is a set of SQL statements to complete specific functions. It is compiled, created and saved in the database. Users can call and execute it by specifying the name of the stored procedure and giving parameters (when necessary). The idea of stored procedure is very simple, which is the code encapsulation and reuse at the level of database SQL language. Generally speaking, a stored procedure is actually a set of SQL statements that can complete some specific operations or functions. The created stored procedure is saved in the data dictionary of the database
2. Advantages and disadvantages of stored procedures
advantage
-
Stored procedures can be encapsulated and can contain complex sql logic.
-
Stored procedures can return values or accept parameters.
-
A stored procedure cannot be run using the SELECT instruction because it is a subroutine, unlike a view table, data table, or user-defined function.
Advantages of using stored procedures: it can enhance the execution efficiency of a group of sql statements, because we must connect to the database before executing each sql statement. Using a group of sql statements can be executed only by connecting to the database once.
shortcoming
-
Stored procedures are often customized to a specific database because different databases support different programming languages. When switching to the database system of other manufacturers, the original stored procedure will fail and need to be rewritten.
-
The performance adjustment and writing of stored procedures are limited by various database systems.
3. Use of stored procedures
3.1 creation and calling of stored procedure
A stored procedure is a piece of code with a name, which is used to complete a specific function. The created stored procedure is saved in the data dictionary of the database.
3.1.1 creation of stored procedure
CREATE PROCEDURE Stored procedure name(parameter list)BEGIN Stored procedure body (a group) sql Statement) END
Among them, BEGIN and END are equivalent to curly braces of methods in Java, and the stored procedure body is equivalent to a set of sql statements. When there is only one sql sentence in the stored procedure body, BEGIN and END can also be omitted, which is equivalent to if and while statements in womenjava. If there is only one sentence in curly braces, curly braces can be omitted, Of course, I personally think that if the stored procedure really has only one sentence, wouldn't it be good to call sql directly?
Note: the parameter list of stored procedure is different from the method in Java. The parameter list in Java only has (parameter type 1, parameter name 1, parameter type 2, parameter name 2...), while the stored procedure list contains three parts: parameter mode, parameter name and parameter type
Introduction to parameter mode
IN: this parameter can be used as input, that is, it needs to pass IN the value when calling the stored procedure
OUT: this parameter can be used as output, that is, it can be used as the return value of the stored procedure
INOUT: this parameter can be used as both input and output, that is, this parameter can be used as both the value passed in when calling and the return value
Parameter example
CREATE PROCEDURE Stored procedure name(IN productName VARCHAR(20))BEGIN Stored procedure body (a group) sql Statement) END
Note: the semicolon is required at the end of each sql statement in the stored procedure body, but the semicolon is used as the end of execution mark by default in mysql, so the execution of the stored procedure ends after the first sql statement is executed! This will cause unexpected errors, so we need to set the end tag and set the end tag at the end of the stored procedure (this operation can be ignored above Mysql8.0). I will use mysql8 to explain here
Syntax: DELIMITER end tag
Example: if you want to set $as the end tag, delete$
3.1.2 calling of stored procedure
CALL stored procedure name (argument list);
3.2 cases of stored procedures
Case 1: create a stored procedure with empty parameters
There were originally two pieces of data in the admin table. Let's insert them in batch
First create a stored procedure
create PROCEDURE insertBatch()BEGIN insert into admin(`name`,`password`) values('Tom','123456'),('Jerry','123456');END
After executing the call, you will find that there are multiple insertBatch in Functions
And then call the stored procedure.
call insertBatch()
Data insertion succeeded!
Case 2: create parameter stored procedure with IN mode
Let's take the following two tables for example, a song Watch and a song table
Song Watch
Song list
Create a stored procedure to query the corresponding song according to the singer name
#To create a stored procedure, we need to singerName Query, and the parameters must be entered, and the parameters are added before them IN,The parameter is singerName,Parameter type is VARCHARcreate PROCEDURE selectSongBySingerName(IN singerName VARCHAR(20))BEGIN sselect singer.`name` as 'Singer name', song.`name` as 'Song name' from singer LEFT JOIN song on singer.id = song.singer_id where singer.`name` = singerName;END#Call CALL selectSongBySingerName('jay Chou ');
Call succeeded
Case 3: create parameter stored procedures with in and out modes
Return the corresponding first song name according to the singer name
Pre knowledge:
- @: represents local variables, also known as user variables
- @@: represents a global variable
#Create a stored procedure. The first parameter is the same as above, and the second parameter: we need to return the output song name, which is used before the parameter OUTCREATE PROCEDURE getSongBySingerName(IN singerName VARCHAR(20),OUT songName VARCHAR(50))BEGIN #there INTO It is the process of assigning the queried value to the variable select song.`name` as 'Song name' INTO songName from singer LEFT JOIN song on singer.id = song.singer_id where singer.`name` = singerName limit 1;END#Call the stored procedure, pass in the parameter "Lin Junjie" and return his first song# @Represents local variable, also known as user variable, @ @ represents global variable CALL getSongBySingerName('lin Junjie ', @ songName)select @songName
Similarly, we can also set a stored procedure that returns the return value of two or more parameters. The procedure is very simple and will not be repeated
Case 4: create a stored procedure with INOUT mode parameters
INOUT mode parameters should be used as little as possible in actual development. Just give an example to understand
Enter two values: the number of singers and the number of songs. In the end, both values are doubled and returned
#singerCount and songCount That is, the variable needs to be returned, so it should be set to INOUTCREATE PROCEDURE dobleCount(INOUT singerCount INT,INOUT songCount INT)BEGIN select count(0) from singer INTO singerCount; select count(0) from song INTO songCount; set singerCount = singerCount * 2; set songCount = songCount * 2;END#Call the method to calculate the result call doblecount (@ singercount, @ songcount) select @ singercount select @ songcount
The original singerCount and songCount were 39 and 93 respectively, and the query results were in line with expectations
3.3. View storage process information
SHOW CREATE PROCEDURE Stored procedure name
3.4. Delete storage process
DROP PROCEDURE Stored procedure name
After talking about the basics of stored procedures, the following energetic partners can enrich themselves or choose to skip
4. Enterprise level project cases
There is a department table t_department, we need to add data in it. Each department has a parent department, the shareholders' meeting is the largest department, and the parentId is - 1
4.1 query all departments
The Department class is automatically generated by mybatis plus generator, as follows
@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@TableName("t_department")@ApiModel(value="Department object", description="")public class Department implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "id") @TableId(value = "id", type = IdType.AUTO) private Integer id; @ApiModelProperty(value = "Department name") private String name; @ApiModelProperty(value = "father id") private Integer parentId; @ApiModelProperty(value = "route") private String depPath; @ApiModelProperty(value = "Enable") private Boolean enabled; @ApiModelProperty(value = "Superior or not") private Boolean isParent;
We add a field children in the Department class, which is a List set composed of departments. Because the parent Department and child Department are nested, the result is a tree structure
@ApiModelProperty(value = "List of sub departments") @TableField(exist = false) private List<Department> children;
Since the query results should make the child departments nested in the parent department layer by layer, how to query? It is not a simple select * from t_department!
We can query the shareholders' meeting of the outermost parent department through parentId = -1, then query the board of directors by taking the id of the shareholders' meeting as parentId, and then query the general office by taking the id of the board of directors as parentId, and then query the finance department and marketing department by taking the general office as parentId, and so on. In fact, it is a recursive call to mysql query
So how to use recursive calls? You can use resultMap for recursive calls
<!-- General query mapping results --> <resultMap id="BaseResultMap" type="com.zlq.server.pojo.Department"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="parentId" property="parentId"/> <result column="depPath" property="depPath"/> <result column="enabled" property="enabled"/> <result column="isParent" property="isParent"/> </resultMap> <resultMap id="departmentWithChildren" type="com.zlq.server.pojo.Department" extends="BaseResultMap"> <collection property="children" ofType="com.zlq.server.pojo.Department" select="com.zlq.server.mapper.DepartmentMapper.getAllDepartment" column="id"> </collection> </resultMap> <!-- General query result column --> <sql id="Base_Column_List"> id , name, parentId, depPath, enabled, isParent </sql> <!--Get all departments--> <select id="getAllDepartment" resultMap="departmentWithChildren"> select <include refid="Base_Column_List"/> from t_department where parentId = #{parentId} </select>
4.2 adding department information
We set the default value in the table. enabled defaults to 1 and isParent defaults to 0
depPath is to look from the back to the front, with its own id at the back, its parent id at the front, its parent id at the front, and so on until it points to the outermost parent department
When adding, we only need to pass in name and parentId parameters to add. For example, I want to add a technical director under the technology department. The adding logic is:
-
Add through the name (Technology Development Department) and parentId (3) from the front end. enabled is 1 by default, isParent is 0 by default, and depPath is empty now
-
Query the depPath (. 1.2.3.12) of its parent department's technology department through the parentId (3) passed from the front end as id
-
Query the id of the new Department (14)
-
Modify the deptpath of the line with id 14 and add. 14 after the depPath of the parent department (the final result is:. 1.2.3.12.14)
-
Modify the isParent of its parent department to 1
Faced with such a complex process, we might as well use stored procedures
-- DEFINER=`root`@`localhost`: Define the user and use the local ip address CREATE DEFINER=`root`@`localhost` PROCEDURE `addDep`(in depName varchar(32),in parentId int,in enabled boolean,out result int,out result2 int) begin -- definition int Type variable, used to store the last inserted department id declare did int; -- definition varchar Type variable, used to store the parent department of the inserted department deptPath declare pDepPath varchar(64); -- Started inserting data into the table insert into t_department set name=depName,parentId=parentId,enabled=enabled; -- System built-in function row_count(),Is the number of affected rows, assigned to result,This value must be dimension 1 if the operation is successful select row_count() into result; -- System built-in function last_insert_id(),For the last inserted data id,Assign to did select last_insert_id() into did; -- result2 For the last inserted Department id set result2=did; -- Query the parent department of the inserted data depPath,Set to pDepPath select depPath into pDepPath from t_department where id=parentId; -- Modify the of the inserted Department depPath,Using splicing, use the parent department in the previous step depPath Splice the last inserted Department id update t_department set depPath=concat(pDepPath,'.',did) where id=did; -- Modify the parent department of the inserted Department isParent The parameter is 1 update t_department set isParent=true where id=parentId; end
We add a field to the Department class because there is result in the result returned by the stored procedure
@ApiModelProperty(value = "Add the returned result, which is used by the stored procedure") @TableField(exist = false) private Integer result;
The sql statement in mybatis is
<select id="addDepartment" statementType="CALLABLE"> call addDep(#{name,mode=IN,jdbcType=VARCHAR},#{parentId,mode=IN,jdbcType=INTEGER}, #{enabled,mode=IN,jdbcType=BOOLEAN},#{result,mode=OUT,jdbcType=INTEGER} ,#{id,mode=OUT,jdbcType=INTEGER}) </select>
DepartmentMapper
void addDepartment(Department department);
DepartmentServiceImpl
@Override public RespBean addDepartment(Department department) { department.setEnabled(true); departmentMapper.addDepartment(department); if (1 ==department.getResult()){ return RespBean.success("Department information added successfully!"); } return RespBean.error("Failed to add department information!"); }
4.3 deleting department information
The logic of deleting a department is also complex. The logic of deleting a department is:
Query whether there are sub departments under the Department to be deleted?
-
If yes, it indicates that there is associated data under the data to be deleted. The result is returned directly and cannot be deleted
-
If not, continue to judge
-
First query whether there are employees in this department?
-
If so, you can't delete it
-
If not, continue to judge
Query whether the parent department of the Department to be deleted has a child department, that is, query whether the Department to be deleted has a department of the same level
- If so, don't move anything
- If not, set isParent of the parent department of the Department you want to delete to 0, because there are no child departments under it
-
-
-- DEFINER=`root`@`localhost`: Define the user and use the local ip address CREATE DEFINER=`root`@`localhost` PROCEDURE `deleteDep`(in did int,out result int)begin declare ecount int;-- Define a int Variable to receive the information of the Department parentId declare pid int; declare pcount int;-- definition int Type variable, used to store the number of departments queried declare a int;-- Incoming through the front end id Query the Department to be deleted and there are no sub departments below. Assign the number to a select count(*) into a from t_department where id=did and isParent=true;-- If the number of departments is not 0, it means that there are sub departments under the Department to be deleted and cannot be deleted! The return value in the setting parameter is-2 if a!=0 then set result=-2; else-- If it exists, query the number of employees in the Department in the employee table associated with it select count(*) into ecount from t_employee where departmentId=did;-- If there are employees in the Department to be deleted, the return value in the setting parameters is-1 if ecount>0 then set result=-1; -- If the Department to be deleted has no employees else-- Query the Department to delete parentId,Assign to pid select parentId into pid from t_department where id=did;-- Delete the specified Department, and there is no sub department under the Department delete from t_department where id=did and isParent=false;-- Assign the number of rows affected to the return value of the parameter select row_count() into result;-- Query whether the parent department of the Department to be deleted has any child departments (query whether the Department to be deleted has any departments of the same level) select count(*) into pcount from t_department where parentId=pid; if pcount=0 then -- If there is no child department, modify the parent department of the deleted department isParent = 0 update t_department set isParent=false where id=pid; end if; end if; end if;end
The sql statements in mybatis are:
<select id="deleteDepartment" statementType="CALLABLE"> call deleteDep(#{id,mode=IN,jdbcType=INTEGER},#{result,mode=OUT,jdbcType=INTEGER}) </select>
DepartmentMapper
void deleteDepartment(Department department);
DepartmentServiceImpl
@Override @Transactional(propagation = Propagation.REQUIRED) public RespBean deleteDepartment(Integer id) { Department department = new Department(); department.setId(id); departmentMapper.deleteDepartment(department); if (-2 == department.getResult()){ return RespBean.error("There are sub departments under this department, which cannot be deleted!"); } if (-1 == department.getResult()){ return RespBean.error("There are employees in this department, unable to delete!"); } if (1 == department.getResult()){ return RespBean.success("Delete succeeded!"); } return RespBean.error("Deletion failed!"); }
Employee module
1. Employee query
1.1 configuring paging plug-ins
There may be hundreds or thousands of employee information. We can't display it on one page, so we need to page it. This time, we use the back-end paging technology and the paging plug-in provided by MybatisPlus
@Configurationpublic class PaginationHelperConfig { @Bean public PaginationInterceptor paginationInterceptor(){ return new PaginationInterceptor(); }}
1.2 configure the public return object of paging
With paging, the results of our query do not need to be placed in the List, but in the public return object of paging
@Data@NoArgsConstructor@AllArgsConstructorpublic class RespPageBean { /* Total records */ private Long total; /* Paging data, put it in the List collection, and use generic? */ private List<?> data;}
Format of global conversion 1.3
The purpose is to process the date data transmitted from the front end. The difference between it and JsonFormat is that the former is to process the data transmitted from the front end and the latter is to process the date data queried by the back end
The date format from the front end is yyyy mm DD
@Componentpublic class DateConvertor implements Converter<String, LocalDate> { @Override public LocalDate convert(String s) { try { return LocalDate.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd")); } catch (Exception e) { e.printStackTrace(); } return null; }}
1.4 modify Employee class
There are two major modifications to employee category
-
Format conversion is added to the returned date class to process the date data queried by the backend
-
Add attributes associated with employees, such as nationality, political outlook, etc
The data to be queried by the front end contains many fields that are not in the Employee entity class (as shown in the figure below)
We added these fields that do not exist in the table. It can be imagined that it is troublesome to deal with the data of employees
@ApiModelProperty(value = "nation") @TableField(exist = false) private Nation nation; @ApiModelProperty(value = "Political outlook") @TableField(exist = false) private PoliticsStatus politicsStatus; @ApiModelProperty(value = "department") @TableField(exist = false) private Department department; @ApiModelProperty(value = "Position level") @TableField(exist = false) private Joblevel joblevel; @ApiModelProperty(value = "position") @TableField(exist = false) private Position position;
1.5 paging query of employees
You can see that the front end has basic query functions, that is, to query all employees, as well as advanced query functions. Among them, political outlook, nationality, position, professional title, employment form and department are all in the entity type field. The entry date is a range of query, and we need to define an array
EmployeeMapper
IPage is the paging object interface in mybatisplus, which provides many methods. Its implementation class is Page in mybatisplus
/** * Paging Page object interface * * @ author hubin * @since 2018-06-09 */public interface IPage<T> extends Serializable { /** * Descending field array * * @ return order by desc field array * @ see #orders() */ @Deprecated default String[] descs() { return null; } /** * Ascending field array * * @ return order by asc field array * @ see #orders() */ @Deprecated default String[] ascs() { return null; } /** * return * @ get the information of sorting fields in positive and negative order */ List<OrderItem> orders(); /** * KEY/VALUE Condition * * @ return ignore */ default Map<Object, Object> condition() { return null; } /** * Auto optimize COUNT SQL [default: true] * * @ return true yes / false no */ default boolean optimizeCountSql() { return true; } /** * count query [default: true] * * @ return true yes / false no */ default boolean isSearchCount() { return true; } /** * Calculates the current paging offset */ default long offset() { return getCurrent() > 0 ? (getCurrent() - 1) * getSize() : 0; } /** * Total pages currently paged */ default long getPages() { if (getSize() == 0) { return 0L; } long pages = getTotal() / getSize(); if (getTotal() % getSize() != 0) { pages++; } return pages; } /** * Do nothing inside * < p > just to avoid any error during json deserialization</p> */ default IPage<T> setPages(long pages) { // to do nothing return this; } /** * Set whether to hit the count cache * * @ param hit whether to hit * @ since 3.3.1 * / default void hitcount (Boolean hit) {} / * * whether to hit the count cache * * @ return whether to hit the count cache * @ since 3.3.1 * / default Boolean ishitcount() {return false;} / * ** Paging record list * * @ return paging object record list * / list < T > getrecords(); / * ** Set paging record list * / iPage < T > setrecords (list < T > records); / * ** Total rows that currently meet the conditions * * @ return total * / long getTotal(); / * ** Set the total number of rows that currently meet the conditions * / iPage < T > settotal (long total); / * ** Get the number of items displayed per page * * @ return the number of items displayed per page * / long getSize(); / * ** Set the number of display bars per page * / iPage < T > SetSize (long size); / * ** Current page, default 1 * * @ return current page * / long getCurrent(); / * ** Set current page * / iPage < T > setcurrent (long current); / * ** Generic conversion of iPage * * @ param mapper conversion function * @ param < R > generic after conversion * @ return iPage after generic conversion * / @ suppresswarnings ("unchecked") default < R > iPage < R > convert (function <? Super T,? Extends r > mapper) {list < R > collect = this. Getrecords(). Stream(). Map (mapper). Collect (tolist()) ); return ((IPage<R>) this). setRecords(collect); }}
In the parameter, we need to pass in the Employee entity class to perform conditional query through the fields inside. LocalDate[] beginDateScope is the range in which we query the employment date. It needs to be defined with the LocalDate array. There must be only two elements in the array
IPage<Employee> listEmployees(Page<Employee> page, @Param("employee") Employee employee, @Param("beginDateScope") LocalDate[] beginDateScope);
-- Set result set mapping, inherit BaseResultMap,Add to Employee Collection in entity class<resultMap id="EmployeeInfo" type="com.zlq.server.pojo.Employee" extends="BaseResultMap"> <collection property="nation" ofType="com.zlq.server.pojo.Nation"> <id column="nid" property="id"/> <result column="nname" property="name"/> </collection> <collection property="politicsStatus" ofType="com.zlq.server.pojo.PoliticsStatus"> <id column="pid" property="id"/> <result column="pname" property="name"/> </collection> <collection property="department" ofType="com.zlq.server.pojo.Department"> <id column="did" property="id"/> <result column="dname" property="name"/> </collection> <collection property="joblevel" ofType="com.zlq.server.pojo.Joblevel"> <id column="jid" property="id"/> <result column="jname" property="name"/> </collection> <collection property="position" ofType="com.zlq.server.pojo.Position"> <id column="poid" property="id"/> <result column="poname" property="name"/> </collection> </resultMap> <!-- Paging query of employee data--> <select id="listEmployees" resultMap="EmployeeInfo"> select e.*, n.id nid, n.`name` nname, ps.id pid, ps.`name` pname, d.id did, d.`name` dname, j.id jid, j.`name` jname, p.id poid, p.`name` poname from t_employee e, t_nation n, t_politics_status ps, t_department d, t_joblevel j, t_position p where e.nationId = n.id and e.politicId = ps.id and e.departmentId = d.id and e.jobLevelId = j.id and e.posId = p.id -- Fuzzy employee name query <if test="null != employee.name and ''!= employee.name"> and e.name like concat('%',#{employee.name},'%') </if> -- Ethnic inquiry <if test="null != employee.nationId"> and e.nationId = #{employee.nationId} </if> -- Political outlook query <if test="null != employee.politicId"> and e.politicId = #{employee.politicId} </if> -- Department query <if test="null != employee.departmentId"> and e.departmentId = #{employee.departmentId} </if> -- Position level query <if test="null != employee.jobLevelId"> and e.jobLevelId = #{employee.jobLevelId} </if> -- Position query <if test="null != employee.posId"> and e.posId = #{employee.posId} </if> <if test="null != beginDateScope and 2==beginDateScope.length"> and e.beginDate between #{beginDateScope[0]} and #{beginDateScope[1]} </if> ORDER BY e.id </select>
IEmployeeService
RespPageBean listEmployeesByPage(Integer currentPage, Integer size, Employee employee, LocalDate[] beginDateScope);
EmployeeServiceImpl
@Override public RespPageBean listEmployeesByPage(Integer currentPage, Integer size, Employee employee, LocalDate[] beginDateScope) { Page<Employee> page = new Page<>(currentPage, size); IPage<Employee> employeePage = employeeMapper.listEmployees(page, employee, beginDateScope); Long total = employeePage.getTotal(); List<Employee> records = employeePage.getRecords(); return new RespPageBean(total,records); }
EmployeeController
@ApiOperation(value = "Paging query of employees, with front-end advanced query function") @GetMapping("/") public RespPageBean listEmployeesByPage(@RequestParam(defaultValue = "1") Integer currentPage, @RequestParam(defaultValue = "10") Integer size, Employee employee, LocalDate[] beginDateScope){ return employeeService.listEmployeesByPage(currentPage,size,employee,beginDateScope); }
2. Addition of employees
When adding an employee, political outlook, nationality, position and professional title are drop-down boxes. Therefore, you need to query all the information of these parameters so that the front-end can call the interface to generate a drop-down list. The job number is incremented by default, so find out the maximum job number. The maximum job number + 1 is the job number of the employee to be added.
@ApiOperation(value = "Get the maximum job number") @GetMapping("/maxWorkId") public RespBean getMaxWorkId(){ return employeeService.getMaxWorkId(); } //Get the maximum job number @ override public respbean getmaxworkid() {/ / query the last workid list < map < string, Object > > maps = employeemapper. Selectmaps (New querywrapper < employee > (). select("max(workId)")); // The last workid queried + 1 int maxworkid = integer parseInt(maps.get(0). get("max(workId)"). toString()) + 1; // The purpose of this format is not to make the previous zero disappear, such as 00000101. If this format is not set, the output result will be 101 string formatmaxworkid = string format("%08d", maxWorkId); return RespBean. success(null,formatMaxWorkId); }
The contract period in the employee field is calculated according to the contract start date and contract end date. The contract period unit is year, with two decimal places reserved
public RespBean insertEmployee(Employee employee){ //Processing contract term, unit: year, with two decimal places reserved. 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)) {return respbean. Success ("employee data inserted successfully!");} return RespBean.error("employee data insertion failed!");}
3. Import and export of employee data
Export of employees
<!--easy poi rely on--><dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-spring-boot-starter</artifactId> <version>4.2.0</version></dependency>
Add the Excel export basic comment @ Excel on the Employee class. If the field length is too long, you can add the width attribute. If it is a time format, you need to add the format attribute
It should be noted that we do not need to export the national id, political face id, Department id, position grade id and position id in the employee table. These fields do not need to be annotated with Excel. We need to export the national name, political appearance name, department name, position grade name and position name. These fields have been associated in the entity class. We need to add @ ExcelEntity to these fields to mark whether to export excel as the entity class
The mapper layer only needs to be responsible for querying employee information according to employee id
/** * Function Description: < br > * "get all employee data" * @ Author: Larry * @ date: 2021 / 4 / 30 3:56 PM * @ param ID * @ return: Java util. List<com. zlq. server. pojo. Employee> */ @Override public List<Employee> getEmployees(Integer id) { return employeeMapper.getEmployees(id); }
@ApiOperation(value = "Export employee data") @GetMapping(value = "/export", produces = "application/octet-stream") public void exportEmployee(HttpServletResponse response) { List<Employee> employeeList = employeeService.getEmployees(null); ExportParams exportParams = new ExportParams("Employee table", "Employee table", ExcelType.HSSF); Workbook workbook = ExcelExportUtil.exportExcel(exportParams, Employee.class, employeeList); ServletOutputStream outputStream = null; try { //Stream form response setHeader("content-type", "application/octet-stream"); // Prevent Chinese garbled code response Setheader ("content disposition", "attachment; filename =" + urlencoder.encode ("employee table. XLS", "UTF-8")); outputStream = response. getOutputStream(); workbook. write(outputStream); } catch (Exception e) { e.printStackTrace(); } finally { if (outputStream != null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
Import of employees
When importing, there are no field IDS based on nationality, political outlook, professional title and position in excel, but only the name attribute of these fields. How to import these field IDS into t_ In the employee table - we need to get the corresponding national id, political appearance id, professional title id, position id and department id.
There are two ways
-
Query the corresponding id in the database according to the value of the name attribute of each associated field. Obviously, continuously querying the database in the loop is very performance-consuming. Each time you insert a piece of data, you need to execute not only the insert statement, but also five more queries, which is very performance-consuming
-
Override the equals and hashCode methods. As long as the value of the name attribute is consistent, it means that the object is consistent. The premise is that the value of the name attribute will not change
We choose the second method
Revise several entity categories such as nationality, political outlook, professional title and position
@ApiOperation(value = "Import employee data") @ApiImplicitParams({@ApiImplicitParam(name = "multipartFile", value = "Upload file", dataType = "MultipartFile")}) @PostMapping("/import") @Transactional public RespBean importEmployee(MultipartFile multipartFile) { ImportParams importParams = new ImportParams(); //Set the first row Title row to avoid the system importing the title row as data importParams.setTitleRows(1); List<Nation> nationList = nationService.list(); List<PoliticsStatus> politicsStatusList = politicsStatusService.list(); List<Department> departmentList = departmentService.list(); List<Joblevel> joblevelList = joblevelService.list(); List<Position> positionList = positionService.list(); try { List<Employee> employeeList = ExcelImportUtil.importExcel(multipartFile.getInputStream(), Employee.class, importParams); //Set the id value of the associated property employeeList.forEach(employee -> { //Set nationalid employee.setNationId(nationList.get(nationList.indexOf(new Nation(employee.getNation().getName()))).getId()); //Set politicId employee.setPoliticId(politicsStatusList.get(politicsStatusList.indexOf(new PoliticsStatus(employee.getPoliticsStatus().getName()))).getId()); //Set departmentId employee.setDepartmentId(departmentList.get(departmentList.indexOf(new Department(employee.getDepartment().getName()))).getId()); //Set jobLevelId employee.setJobLevelId(joblevelList.get(joblevelList.indexOf(new Joblevel(employee.getJoblevel().getName()))).getId()); //Set positionId employee.setPosId(positionList.get(positionList.indexOf(new Position(employee.getPosition().getName()))).getId()); }); if (employeeService.saveBatch(employeeList)){ return RespBean.success("Employee import succeeded!"); } return RespBean.error("Employee import failed!"); } catch (Exception e) { e.printStackTrace(); } return RespBean.error("Employee import failed!"); }
Thought the above employee Setnationid() as an example
employeeList.forEach(employee -> { //Get the index of the nation name in each employee object in the nationList int nationIndex = nationList.indexOf(new Nation(employee.getNation().getName())); //Get the id of the corresponding index of the nation name in the nationList, which is the id we want to set Integer nationId = nationList.get(nationIndex).getId(); employee.setNationId(nationId); });
mail serve
Our email service uses rabbitmq as a message middleware to deliver employee information. The benefits of using rabbitmq are decoupling, asynchrony and traffic peak clipping
1. Sending of simple form mail
First, create a simple mail service, add an employee, and send a welcome email to the employee's mailbox after adding it successfully
- First create a new service module, yeb mail
- Add dependency
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xxxx</groupId> <artifactId>yeb-mail</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>com.zlq</groupId> <artifactId>yeb</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!--rabbitmq rely on--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!--mail rely on--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <!--thymeleaf rely on--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--server rely on--> <dependency> <groupId>com.zlq</groupId> <artifactId>yeb-server</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> </project>
- Modify profile
server: port: 8082 spring: #rabbitmq configuration rabbitmq: username: admin password: admin host: 47.93.242.4 port: 5672 #mail configuration mail: host: smtp.qq.com username: 804492004@qq.com #QQ email password: vinsxwuaqlhbbgac #authorization token protocol: smtp properties.mail.smtp.auth: true properties.mail.smtp.port: 465 properties.mail.smtp.starttls.enable: true properties.mail.smtp.starttls.required: true properties.mail.smtp.ssl.enable: true default-encoding: utf-8
**There is a pit here: * * you must put the main startup class under the mail folder, otherwise the project cannot be started! In addition, the DataSourceAutoConfiguration class should be excluded from the main startup class, because the module does not need to connect to the database, but yeb server is introduced, which has mysql dependency and needs to be excluded:
-
Modify the method of adding employees
When the employee is successfully added, we need to add the employee information to the message queue of rabbitmq, and then in the mail module, obtain and consume the employee information through rabbitmq, obtain the employee information, and send the email to the employee mailbox
-
Prepare mail template
Create the templates directory in the resource directory, and the template engine will find html pages from this directory by default
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.theymeleaf.org"> <head> <meta charset="UTF-8"> <title>Welcome email</title> </head> <body> welcome <span th:text="${name}"></span>To join the extended family, your entry information is as follows: <table border="0.5"> <tr> <td>full name</td> <td th:text="${name}"></td> </tr> <tr> <td>position</td> <td th:text="${posName}"></td> </tr> <tr> <td>title</td> <td th:text="${joblevelName}"></td> </tr> <tr> <td>department</td> <td th:text="${departmentName}"></td> </tr> </table> <p> The purpose of our company's work is strict, innovative and honest. Your joining will bring us fresh blood, innovative thinking and establish a good company image for us!I hope we can work together, keep pace with the times, unite and cooperate in our future work!At the same time, I also wish you a happy job in our company and realize your life value!I hope to work together in the future!</p> </body> </html>
- Create consumer information in rabbitmq and send email
package com.zlq.mail.receiver; import com.zlq.server.pojo.Employee; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.boot.autoconfigure.mail.MailProperties; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import javax.annotation.Resource; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.Date; /** * @ProjectName:yeb * @Package:com.zlq.mail * @ClassName: MailReceiver * @description: * @author: LiQun * @CreateDate:2021/5/5 3:19 afternoon */ @Component public class MailReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class); @Resource private JavaMailSender javaMailSender; @Resource private MailProperties mailProperties; @Resource private TemplateEngine templateEngine; @RabbitListener(queues = "mail.welcome") //Monitor mail Messages in the welcome queue public void sendMail(Employee employee){ MimeMessage message = javaMailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(message); try { //Set sender messageHelper.setFrom(mailProperties.getUsername()); //Set recipient messageHelper.setTo(employee.getEmail()); //Set sending subject messageHelper.setSubject("Welcome email"); //Set sending date messageHelper.setSentDate(new Date()); //Set message content Context context = new Context(); //The following parameters correspond to mail Parameters of template engine in HTML 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); messageHelper.setText(mail,true); //Parameter 1: mail parameter 2: whether it is html mail //Send mail javaMailSender.send(message); } catch (MessagingException e) { e.printStackTrace(); LOGGER.error("Mail sending failed ========>",e.getMessage()); } } }
Summary: This is just a simple message queue email sending form. The message queue will be optimized below because two pits have been stepped on before. Pit 1: directory structure (big pit) and pit 2: the wrong query method of Employee in EmployeeServiceImpl, resulting in no query of department position, rank and other information. Cause your progress to slow down!
2. Optimization of message delivery reliability of rabbitmq production end
In the actual development, to meet the reliability of message delivery, in rabbitmq, how to ensure that messages are delivered normally and consumed?
How to ensure that messages are not lost? How to ensure that messages will not be sent repeatedly?
Method 1: the message is stored in the library and the message status is marked
Project realization process:
-
When sending a message, the current message data is stored in the database, and the delivery status is status= 0
-
Enable the message confirmation callback mechanism. The confirmation is successful, and the update delivery status is message delivery success status = 1
-
Start the scheduled task and re deliver the failed message. Retry more than 3 times, update the delivery status to delivery failure status = 2
The detailed steps in the figure above are analyzed as follows
Normal link flow
-
Step 1: the producer produces the message and writes the message marking (marking: the message contains additional marking fields to show the message status) into the mysql database. At this time, the status of the message is 0
-
Step 2: the producer sends messages to the queue through the switch
-
step3: monitor whether the message is sent successfully through the message callback mechanism
-
step4: if the first sending is successful - set the message status to 1
Abnormal link flow
-
step5: start a distributed scheduled task. If the first sending fails, the scheduled task will regularly query the message with status 0
-
Step 6: when the scheduled task queries the message with status 0, it will re apply for sending the message
-
step7: every time the scheduled task tries to send the message, it will add one count (message sending times) in the message. When count > = 3, it will set the status to 2, which means that the message sending fails
Disadvantages of message marking and dropping database: secondary operations are performed on the database, that is, business data and message record data are inserted. There is a secondary db operation, and there is a database performance bottleneck under high concurrent operations
Delivery method: second check
There are production messages at the upstream service production end and consumers at the downstream service consumption end
- Warehousing business data
- The production side sends messages to the rabbbitmq queue
- Delayed delivery of secondary messages with interval time difference
- The consumer confirms after receiving the message
- The consumer sends confirmation information to the message queue to confirm whether the information is a regenerated message. The message is attached with the id value of the message received by the consumer
- Listen to the confirmation message sent by the consumer through the callback service
- If you hear confirmation information, put the message into the database
- If the second delayed delivery message is detected, check the database for the delayed delivery message id
- If the id of the second delayed delivery message exists in the database, it indicates that the message has been delivered successfully
- If the id of the secondary delayed delivery message does not exist in the database, it indicates that the message has not been delivered successfully, send RPC communication and start from the first step again
The main purpose of scheme 2 is to reduce database operations and improve concurrency. In high concurrency scenarios, the most concern is not 100% successful message delivery, but to ensure performance and withstand such a large amount of concurrency. Therefore, if you can reduce database operations, you can minimize them and compensate asynchronously
We choose the simple way 1
-
Define message status constants
In the actual development, in rabbitmq module, we have a large number of constants that need to be defined, so we encapsulate these constants into classes
public class MailConstants { //Message is in delivery state public static final Integer DELIVERING = 0; //Message delivery succeeded public static final Integer SUCCESS = 1; //Message delivery failed public static final Integer FAILURE = 2; //max retries public static final Integer MAX_TRY_COUNT = 3; //Message timeout public static final Integer MSG_TIMEOUT = 1; //queue public static final String MAIL_QUEUE_NAME = "mail.queue"; //Switch public static final String MAIL_EXCHANGE_NAME = "mail.exchange"; //Routing key public static final String MAIL_ROUTING_KEY_NAME = "mail.routing.key"; }
-
Carry the specific parameters of the message to be sent into the mysql database
This is the database table we created to store message parameters
Save the message to be sent into the database. The table in the database is created by ourselves, which contains the message status, routing key, switch name, queue name, etc
//Modify employee insertion method public RespBean insertEmployee(Employee employee) { //Processing contract period, unit: year, with two decimal places reserved LocalDate beginContract = employee.getBeginContract(); LocalDate endContract = employee.getEndContract(); //Calculate the number of days from the contract start date and to the same end date 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)) { //If the employee information is successfully inserted, the newly inserted employee information is sent to the message queue Employee emp = employeeMapper.getEmployees(employee.getId()).get(0); //Set the unique identification of the message through uuid String msgId = UUID.randomUUID().toString(); //Message drop Library messageInDB(emp,msgId); //When sending a message, parameter 1: switch, parameter 2: routing key, parameter 3: id, which is also the unique identification of the message rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME,MailConstants.MAIL_ROUTING_KEY_NAME,emp,new CorrelationData(msgId)); return RespBean.success("Employee data inserted successfully!"); } return RespBean.error("Failed to insert employee data!"); } /** * Function Description: < br > * 〈Message drop * @Author: Larry * @Date: 2021/5/6 11:12 morning * @param emp Employee information * @param msgId Message unique identifier * @return: void */ /* Save the message to be sent into the database, and the tables in the database are created by ourselves, It contains the message status, routing key, switch name, queue name, etc */ public void messageInDB(Employee emp,String msgId){ MailLog mailLog = new MailLog(); mailLog.setMsgId(msgId); mailLog.setEid(emp.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()); //Insert message into mysql database mailLogMapper.insert(mailLog); }
-
Modify mail module
- Set the name of the created queue to the queue name set in the constant class
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class YebMailApplication { public static void main(String[] args) { SpringApplication.run(YebMailApplication.class,args); } @Bean public Queue queue(){ return new Queue(MailConstants.MAIL_QUEUE_NAME); } }
-
Write rabbitmq configuration class
package com.zlq.server.config; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.zlq.server.common.MailConstants; import com.zlq.server.pojo.MailLog; import com.zlq.server.service.IMailLogService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; import java.lang.annotation.Native; /** * @ProjectName:yeb * @Package:com.zlq.server.config * @ClassName: RabbitmqConfig * @description: * @author: LiQun * @CreateDate:2021/5/5 11:47 afternoon */ @Configuration public class RabbitmqConfig { @Resource private CachingConnectionFactory cachingConnectionFactory; @Resource private IMailLogService mailLogService; private static final Logger LOGGER = LoggerFactory.getLogger(RabbitmqConfig.class); //Inject the RabbitTemplate object into the spring container, and configure the start confirmation message callback and message failure callback @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory); /** * Whether the callback message arrives or not * Parameter 1: Message unique ID * Parameter 2: confirm the result and whether the message is sent successfully * Parameter 3: failure reason */ rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { String msgId = correlationData.getId(); if (ack) { LOGGER.info("Message sent successfully============>" + msgId); //Monitor whether the message is sent successfully. If successful, change the status to 1 mailLogService.update(new UpdateWrapper<MailLog>().set("status", 1).eq("msgId", msgId)); } else { LOGGER.info("Message sent to queue fail============>" + msgId); } }); /** * Message failure callback */ rabbitTemplate.setReturnsCallback(returnedMessage -> LOGGER.info("{}=====>Message sent to queue Time failure", returnedMessage.getMessage().getBody())); return rabbitTemplate; } //Bind queue @Bean public Queue queue(){ return new Queue(MailConstants.MAIL_QUEUE_NAME,true); } //Bind switch @Bean public DirectExchange directExchange(){ return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME); } //Binding queue and the relationship between routing key and switch @Bean public Binding binding(){ return BindingBuilder.bind(queue()).to(directExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME); } }
-
Enable the confirmation message callback and message failure callback in the configuration file
-
Start the scheduled task of mail sending
In order to ensure the reliability of messages, we need to start the scheduled task of email sending. The purpose is:
Be careful not to forget to add the timed task annotation @ enableshcheduling on the main startup class
The business logic is:
- Mail sending scheduled task, try to send it every ten seconds,
- If the message status is 0 and the retry time is less than the current time, it needs to be sent again (it is also well understood here that the current time changes and the retry time remains unchanged. It can only be sent after the current time passes the retry time)
- If the number of retries exceeds 3, it will be updated as delivery failure. Do not retry again
- Update retry times, update time, retry time
- send message
package com.zlq.server.task; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.zlq.server.common.MailConstants; import com.zlq.server.pojo.Employee; import com.zlq.server.pojo.MailLog; import com.zlq.server.service.IEmployeeService; import com.zlq.server.service.IMailLogService; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.List; @Component public class MailTask { @Resource private RabbitTemplate rabbitTemplate; @Resource private IMailLogService mailLogService; @Resource private IEmployeeService employeeService; @Scheduled(cron = "0/10 * * * * ?") public void mailTask() { //If the query message status is 0 and the retry time is less than the current time, only the messages that meet this condition need to be sent again List<MailLog> mailLogList = mailLogService.list(new QueryWrapper<MailLog>() .eq("status", 0) .lt("tryTime", LocalDateTime.now())); //If the number of retries exceeds 3, it will be updated as delivery failure and will not be retried again (the status will be updated to 2) mailLogList.forEach(mailLog -> { if (3 <= mailLog.getCount()) { mailLogService.update(new UpdateWrapper<MailLog>().set("status", 2) .eq("msgId", mailLog.getMsgId())); } //Update retry times, update time, retry time mailLogService.update(new UpdateWrapper<MailLog>() .set("count", mailLog.getCount() + 1) .set("updateTime", LocalDateTime.now()) .set("tryTime", LocalDateTime.now().plusMinutes(MailConstants.MSG_TIMEOUT)) .eq("msgId", mailLog.getMsgId())); //Get the employee information in the sent message Employee employee = employeeService.getEmployees(mailLog.getEid()).get(0); //send message rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, employee, new CorrelationData(mailLog.getMsgId())); }); } }
Test:
- Normal message sending
[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-qafqpndm-1621242835615) (/ users / Liqun / library / Application Support / typera user images / image-20210506171543782. PNG)]
-
When inserting employee information, the first sending fails but the scheduled task is sent successfully
The switch name sent to the message queue when inserting employees is modified here. Rabbitmq doesn't know where to send messages because there is no corresponding switch! Therefore, the message cannot be sent successfully, and rabbitmq cannot listen to the confirmation message of successful message sending. Then the status of the message in the database will not change to 1. It is necessary to wait for the scheduled task to process the message and send the message to the rabbitmq message queue through the scheduled task
-
The first transmission fails and the scheduled task also fails
Failed to send three times, and the message is marked as sending failure
Here, the name of the switch sent to the message queue when inserting employees and the name of the switch in the scheduled task are modified. Therefore, the message cannot be sent successfully for the first time, and the scheduled task cannot be sent successfully! If the scheduled task tries three times, the status will be changed to 2 and the message will be sent again.
3. Optimization of idempotency of rabbitmq consumer message consumption
How to ensure that messages will not be consumed repeatedly in production? If duplicate messages come in at the same time, we need to check the idempotency of the message to make it consume only one message
There may be a performance bottleneck when using mysql at high speed. Redis cache is used here First, go to redis to check whether the current message id exists. If it exists, it indicates that it has been consumed and returned directly. If it does not exist, send the message normally and store the message id in IDS. A manual confirmation message is required
Implementation steps
-
Add manual confirmation function to the configuration file
-
Modify mail sending business logic
Basic modification ideas:
- Judge whether msgId is included in redis. If yes, it indicates that the message has been consumed. Confirm it manually and return to the end method
- If msgId is not included in redis, consume the message normally and send an email, store the msgId in redis and confirm the message manually. If there is any exception, you also need to confirm it manually and return it to the queue
package com.zlq.mail.receiver; import com.rabbitmq.client.Channel; import com.zlq.server.common.MailConstants; import com.zlq.server.pojo.Employee; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.boot.autoconfigure.mail.MailProperties; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import javax.annotation.Resource; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.IOException; import java.util.Date; /** * @ProjectName:yeb * @Package:com.zlq.mail * @ClassName: MailReceiver * @description: * @author: LiQun * @CreateDate:2021/5/5 3:19 afternoon */ @Component public class MailReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class); @Resource private JavaMailSender javaMailSender; @Resource private MailProperties mailProperties; @Resource private TemplateEngine templateEngine; @Resource private RedisTemplate redisTemplate; /* Why use Message to get employee class? Because in addition to obtaining the Employee class, we also need to obtain, for example, Message serial number, msgId, etc */ @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME) public void sendMail(Message message, Channel channel) { //Get employee class Employee employee = (Employee) message.getPayload(); MessageHeaders headers = message.getHeaders(); //Get message sequence number long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG); //Gets the msgId of the message String msgId = (String) headers.get("spring_returned_message_correlation"); HashOperations hashOperations = redisTemplate.opsForHash(); try { MimeMessage msg = javaMailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(msg); //Judge whether msgId exists in redis. If yes, return it directly if (hashOperations.entries("mail_log").containsKey(msgId)) { LOGGER.error("The message has been consumed========>", msgId); //Manual confirmation message channel.basicAck(tag, false); return; } //Set sender messageHelper.setFrom(mailProperties.getUsername()); //Set recipient messageHelper.setTo(employee.getEmail()); //Set sending subject messageHelper.setSubject("Welcome email"); //Set sending date messageHelper.setSentDate(new Date()); //Set message content Context context = new Context(); //The following parameters correspond to mail Parameters of template engine in HTML 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); messageHelper.setText(mail, true); //Parameter 1: mail parameter 2: whether it is html mail //Send mail javaMailSender.send(msg); LOGGER.info("Mail sent successfully ========>"); //After the email is sent successfully, store the msgId ID in redis hashOperations.put("mail_log", msgId, "ok"); //Manual confirmation message channel.basicAck(tag, false); } catch (Exception e) { /** * Manually confirm the message, reject the received message and return to the queue, that is, if the consumption of the message is abnormal, the message will be returned to the queue * @tag message number * @multiple Process multiple * @requeue Whether to return to the queue. If false, the message will not be retransmitted, Will put the message in the dead letter queue. If true, it will cause infinite retransmissions, resulting in an endless loop. It is not recommended to add try catch */ try { channel.basicNack(tag, false, true); } catch (IOException ioException) { ioException.printStackTrace(); } e.printStackTrace(); LOGGER.error("Mail sending failed ========>", e.getMessage()); } } }
Generally speaking, the idempotency of the message is handled in this way simply, that is, the message's msgId is stored in the cache, and the cached msgId is used to judge whether it is equal to the msgId of the message to be consumed, so as to deal with the repeated operation of the message
Other methods: use distributed locks and dead letter queues
To be optimized: if the mailbox filled in by the new employee's mailbox is legal and the front-end verification passes, but the back-end can't find the corresponding mailbox when sending mail, javax.com will appear mail. Sendfailedexception exception, and return the message to rabbitmq queue, and then rabbitmq will resend the message, causing an endless loop! This leads to the continuous retry of the service until the disk memory is exhausted and downtime occurs
- Optimization method 1: cancel channel basicNack(tag, false, true); Try catch in, but once the email is sent in error, it can't be sent again. It's not reliable at all!
-
Optimization method 2: limit the number of messages sent + manual ack
-
Optimization method 3: create a dead letter queue and put the messages that cannot be sent into the dead letter queue. The messages that cannot be sent by the dead letter queue will be intervened manually or notified to the administrator
4. Optimization of using dead letter queue
When are queues and switches created in rabbitmq? After creating the switch and queue in our application and binding the relationship between the switch and queue, start the application to create the queue, but the premise is that the creation of the queue requires a corresponding consumer in the queue to listen to the queue
Switches are created when messages are first sent to the switch
The optimization method selects the above method 3. The specific idea is to put the abnormal message into the dead letter queue to ensure that the work queue will not generate message accumulation for normal consumption. If the message in the dead letter queue is sent abnormally again, manual intervention will be carried out to send the message to the system administrator
-
RabbitmqConfig of server module adds dead letter switch and dead letter queue, and binds work queue and dead letter switch
package com.zlq.server.config; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.zlq.server.common.MailConstants; import com.zlq.server.pojo.MailLog; import com.zlq.server.service.IMailLogService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; import java.lang.annotation.Native; import java.util.HashMap; import java.util.Map; /** * @ProjectName:yeb * @Package:com.zlq.server.config * @ClassName: RabbitmqConfig * @description: * @author: LiQun * @CreateDate:2021/5/5 11:47 afternoon */ @Configuration public class RabbitmqConfig { @Resource private CachingConnectionFactory cachingConnectionFactory; @Resource private IMailLogService mailLogService; private static final Logger LOGGER = LoggerFactory.getLogger(RabbitmqConfig.class); /* Inject the RabbitTemplate object into the spring container, and configure the start confirmation message callback and message failure callback */ @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory); /** * Whether the callback message arrives or not * Parameter 1: Message unique ID * Parameter 2: confirm the result and whether the message is sent successfully * Parameter 3: failure reason */ rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { String msgId = correlationData.getId(); if (ack) { LOGGER.info("Message sent successfully============>" + msgId); //Monitor whether the message is sent successfully. If successful, change the status to 1 mailLogService.update(new UpdateWrapper<MailLog>().set("status", 1).eq("msgId", msgId)); } else { LOGGER.info("Message sent to queue fail============>" + msgId); } }); /** * Message failure callback */ rabbitTemplate.setReturnsCallback(returnedMessage -> LOGGER.info("{}=====>Message sent to queue Time failure", returnedMessage.getMessage().getBody())); return rabbitTemplate; } /** * Function Description: < br > * 〈Create dead letter queue * @Author: Larry * @Date: 2021/5/17 10:48 morning * @param * @return: org.springframework.amqp.core.Queue */ @Bean public Queue getDeadQueue() { return new Queue(MailConstants.DEAD_MAIL_QUEUE_NAME); } /** * Function Description: < br > * 〈Create dead letter switch * @Author: Larry * @Date: 2021/5/17 10:50 morning * @param * @return: org.springframework.amqp.core.Exchange */ @Bean public Exchange getDeadExchange() { return ExchangeBuilder.directExchange(MailConstants.DEAD_MAIL_EXCHANGE_NAME).durable(true).build(); } /** * Function Description: < br > * 〈Bind dead letter queue to dead letter switch * @Author: Larry * @Date: 2021/5/17 10:50 morning * @param * @return: org.springframework.amqp.core.Binding */ @Bean public Binding bindDead() { return BindingBuilder.bind(getDeadQueue()).to(getDeadExchange()).with(MailConstants.DEAD_MAIL_ROUTING_KEY_NAME).noargs(); } /** * Function Description: < br > * 〈Create work queue * @Author: Larry * @Date: 2021/5/17 10:50 morning * @param * @return: org.springframework.amqp.core.Queue */ @Bean public Queue getNormalQueue() { Map args = new HashMap(); //When the message sending is abnormal, the message needs to be routed to the switch and routing key. Here, it is directly sent to the dead letter queue args.put("x-dead-letter-exchange", MailConstants.DEAD_MAIL_EXCHANGE_NAME); args.put("x-dead-letter-routing-key", MailConstants.DEAD_MAIL_ROUTING_KEY_NAME); //When creating a queue, bind the dead letter to the queue return QueueBuilder.durable(MailConstants.MAIL_QUEUE_NAME).withArguments(args).build(); } /** * Function Description: < br > * 〈Create work switch * @Author: Larry * @Date: 2021/5/17 10:50 morning * @param * @return: org.springframework.amqp.core.Exchange */ @Bean public Exchange getNormalExchange() { return ExchangeBuilder.directExchange(MailConstants.MAIL_EXCHANGE_NAME).durable(true).build(); } /** * Function Description: < br > * 〈Bind work queue to work switch * @Author: Larry * @Date: 2021/5/17 10:50 morning * @param * @return: org.springframework.amqp.core.Binding */ @Bean public Binding bindNormal() { return BindingBuilder.bind(getNormalQueue()).to(getNormalExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME).noargs(); } }
-
MailReceiver class is used to receive messages from work queues and send mail
import javax.annotation.Resource; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.IOException; import java.util.Date; /** * @ProjectName:yeb * @Package:com.zlq.mail * @ClassName: MailReceiver * @description: * @author: LiQun * @CreateDate:2021/5/5 3:19 afternoon */ @Component public class MailReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class); @Resource private JavaMailSender javaMailSender; @Resource private MailProperties mailProperties; @Resource private TemplateEngine templateEngine; @Resource private RedisTemplate redisTemplate; /** * Function Description: < br > * 〈Mail sending * * @param message * @param channel * @Author: Larry * @Date: 2021/5/5 10:23 morning * @return: void */ /* Why use Message to get employee class? Because in addition to obtaining the Employee class, we also need to obtain, for example, Message serial number, msgId, etc Basic realization idea: 1. Judge whether msgId is included in redis. If yes, it indicates that the message has been consumed. Confirm it manually and return to the end method 2. If msgId is not included in redis, you can normally consume messages and send emails, and store msgId in redis, And manually confirm the message. If there are exceptions, you also need to manually confirm and return to the queue */ @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME) public void sendMail(Message message, Channel channel) { //Get employee class Employee employee = (Employee) message.getPayload(); MessageHeaders headers = message.getHeaders(); //Get message sequence number long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG); //Gets the msgId of the message String msgId = (String) headers.get("spring_returned_message_correlation"); HashOperations hashOperations = redisTemplate.opsForHash(); try { //Judge whether msgId exists in redis. If yes, return it directly if (hashOperations.entries("mail_log").containsKey(msgId)) { LOGGER.error("The message has been consumed========>", msgId); //Manual confirmation message channel.basicAck(tag, false); return; } MimeMessage msg = javaMailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(msg); //Set sender messageHelper.setFrom(mailProperties.getUsername()); //Set recipient messageHelper.setTo(employee.getEmail()); //Set sending subject messageHelper.setSubject("Welcome email"); //Set sending date messageHelper.setSentDate(new Date()); //Set message content Context context = new Context(); //The following parameters correspond to mail Parameters of template engine in HTML 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); messageHelper.setText(mail, true); //Parameter 1: mail parameter 2: whether it is html mail // int o = 1/0; //Send mail javaMailSender.send(msg); LOGGER.info("Mail sent successfully ========>"); //After the email is sent successfully, store the msgId ID in redis hashOperations.put("mail_log", msgId, "ok"); //Manual confirmation message channel.basicAck(tag, false); /** * Manually confirm the message, reject the received message and return to the queue, that is, if the consumption of the message is abnormal, the message will be returned to the queue * @tag message number * @multiple Process multiple * @requeue Whether to return to the queue. If false, the message will not be re sent and will be put into the dead letter queue. If true, infinite retries will result in an endless loop. Try catch is not recommended */ } catch (Exception e) { try { channel.basicNack(tag, false, false); } catch (Exception exception) { if (exception instanceof MailSendException){ LOGGER.error("The mailbox was not found============>" + e.getMessage()); } exception.printStackTrace(); } e.printStackTrace(); LOGGER.error("Failed to send mail for the first time ========>", e.getMessage()); } }
-
The DeadQueueMailReceiver class is used to receive messages from the dead letter queue and send mail
package com.zlq.mail.receiver; import com.rabbitmq.client.Channel; import com.zlq.mail.receiver.MailReceiver; import com.zlq.server.common.MailConstants; import com.zlq.server.pojo.Employee; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.boot.autoconfigure.mail.MailProperties; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import javax.annotation.Resource; import javax.mail.internet.MimeMessage; import java.io.IOException; import java.util.Date; /** * @ProjectName:yeb * @Package:com.zlq.mail.task * @ClassName: DeadQueueMailTask * @description: * @author: LiQun * @CreateDate:2021/5/17 11:32 morning */ //Listen for messages in the dead letter queue @Component public class DeadQueueMailReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(MailReceiver.class); @Resource private JavaMailSender javaMailSender; @Resource private MailProperties mailProperties; @Resource private TemplateEngine templateEngine; @Resource private RedisTemplate redisTemplate; @RabbitListener(queues = MailConstants.DEAD_MAIL_QUEUE_NAME) public void handleDeadQueue(Message message, Channel channel) { //Get employee class Employee employee = (Employee) message.getPayload(); MessageHeaders headers = message.getHeaders(); //Get message sequence number long tag = (long) headers.get(AmqpHeaders.DELIVERY_TAG); //Gets the msgId of the message String msgId = (String) headers.get("spring_returned_message_correlation"); HashOperations hashOperations = redisTemplate.opsForHash(); try { //Judge whether msgId exists in redis. If yes, return it directly if (hashOperations.entries("mail_log").containsKey(msgId)) { LOGGER.error("Messages in the dead letter queue have been consumed========>", msgId); //Manual confirmation message channel.basicAck(tag, false); return; } MimeMessage msg = javaMailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(msg); //Set sender messageHelper.setFrom(mailProperties.getUsername()); //Set recipient messageHelper.setTo(employee.getEmail()); //Set sending subject messageHelper.setSubject("Welcome email"); //Set sending date messageHelper.setSentDate(new Date()); //Set message content Context context = new Context(); //The following parameters correspond to mail Parameters of template engine in HTML 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); messageHelper.setText(mail, true); //Parameter 1: mail parameter 2: whether it is html mail //Send mail javaMailSender.send(msg); LOGGER.info("Mail resend succeeded ========>"); //After the email is sent successfully, store the msgId ID in redis hashOperations.put("mail_log", msgId, "ok"); channel.basicAck(tag, false); /** * Manually confirm the message, reject the received message and return to the queue, that is, if the consumption of the message is abnormal, the message will be returned to the queue * @tag message number * @multiple Process multiple * @requeue Whether to return to the queue. If false, the message will not be re sent and will be put into the dead letter queue. If true, infinite retries will result in an endless loop. Try catch is not recommended */ } catch (Exception e) { try { channel.basicNack(tag, false, false); } catch (Exception exception) { if (exception instanceof MailSendException){ LOGGER.error("The mailbox was not found============>" + e.getMessage()); } exception.printStackTrace(); } LOGGER.error("Failed to send mail for the second time ========>", e.getMessage()); } } }
-
The configuration class is also written in the mail module, which is exactly the same as the RabbitmqConfig content in the server module
Extension: what is ACK, what is manual ACK and what is automatic ack
ack is a message confirmation callback mechanism. When a message producer sends the message successfully, or the consumer receives the message and consumes it, it feeds back to RabbitMQ. RabbitMQ confirms that the message has been added or deleted after receiving the feedback.
[the external chain image transfer fails, and the source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-sEV4Fb0p-1621242835619)(... /... /... /... / users / liquun / library / Application Support / typera user images / image-20210517164653924. PNG)]
Personal center function
Modify personal information
1. Modify user password
@Override public RespBean updatePassword(Integer adminId, String oldPass, String newPass) { Admin admin = adminMapper.selectById(adminId); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); if (encoder.matches(oldPass, admin.getPassword())) { String newPassword = encoder.encode(newPass); admin.setPassword(newPassword); if (adminMapper.updateById(admin) == 1) { return RespBean.success("Password modification succeeded!"); } return RespBean.error("Password modification failed!"); } return RespBean.error("Password input error!"); }
2. Update operation user information
The only thing to note is to modify the authentication information
@ApiOperation(value = "Update current user information") @PostMapping("/admin/info") public RespBean updateAdminInfo(@RequestBody Admin admin, Authentication authentication){ //Modify admin information if (adminService.updateById(admin)){ //Modify authentication information UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(admin,authentication.getCredentials(),authentication.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(token); return RespBean.success("User information updated successfully!"); } return RespBean.error("User information update failed!");
The test found an exception
The exception is a JSON deserialization exception
Reason: This is because our Admin entity class implements the UserDetails interface and rewrites the getAuthorities() method, but the Admin entity class has no corresponding collection <? Extends grantedauthority > attribute, and cannot create a collection containing <? Constructor for the extends grantedauthority > attribute. JSON cannot be deserialized, resulting in an error
Solution: customize the deserialization class and use the custom deserialization class in the getAuthorities() method definition
Annotate the getAuthorities method in the Admin class
@JsonDeserialize(using = AdminAuthorityDeserializer.class)
Write a Json deserialization parser for custom Authority
/** * @ProjectName:yeb * @Package:com.zlq.server.config * @ClassName: AdminAuthorityDeserializer * @description: Json deserialization parser with custom Authority * @author: LiQun * @CreateDate:2021/5/8 8:54 afternoon */ public class AdminAuthorityDeserializer extends JsonDeserializer { @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec(); JsonNode jsonNode = mapper.readTree(jsonParser); List<GrantedAuthority> authorityList = new LinkedList<>(); Iterator<JsonNode> elements = jsonNode.elements(); while (elements.hasNext()){ JsonNode next = elements.next(); JsonNode authority = next.get("authority"); authorityList.add(new SimpleGrantedAuthority(authority.asText())); } return authorityList; } }
Update Avatar
Fastdfs is used to update the avatar. In order to learn fastdfs, I also specially found a tutorial on the Internet. I use docker to deploy fastdfs. The traditional method is too cumbersome
The tutorial of docker deploying fastdfs has been sorted out, and the blog connection is as follows:
https://blog.csdn.net/weixin_49149614/article/details/116572085?spm=1001.2014.3001.5501
Introduction to fastDfs
FastDFS is an open source lightweight distributed file system. It manages files. Its functions include file storage, file synchronization, file access (file upload, file download), etc. It solves the problems of mass storage and load balancing. It is especially suitable for online services based on documents, such as photo album websites, video websites and so on.
fastDfs benefits
FastDFS is tailor-made for the Internet. It fully considers redundant backup, load balancing, linear capacity expansion and other mechanisms, and pays attention to high availability, high performance and other indicators. It is easy to build a set of high-performance file server cluster using FastDFS to provide file upload, download and other services.
fastDfs structure
FastDFS server has two roles: tracker and storage node. The tracker mainly does scheduling work and plays the role of load balancing in access.
fastDfs upload process
- The Client finds available storage servers through the Tracker server.
- The Tracker server returns the IP address and port number of an available Storage server to the Client.
- The Client directly establishes a connection with one of the storage servers through the IP address and port returned by the Tracker server and uploads files.
- After the upload is completed, the Storage server returns a file ID to the Client, and the file upload is completed.
fastDfs download process
- The Client finds the Storage server where the file to be downloaded is located through the Tracker server.
- The Tracker server returns the IP address and port number of a Storage server containing the specified file to the Client.
- The Client directly establishes a connection with one of the storage servers through the IP address and port returned by the Tracker server, and specifies the files to be downloaded.
- Download File succeeded.
Synchronization mechanism
Storage servers in the same group are peer-to-peer, and file upload, deletion and other operations can be carried out on any storage server; File synchronization is only conducted between storage servers in the same group. push mode is adopted, that is, the source server is synchronized to the target server; Only the source data needs to be synchronized, and the backup data does not need to be synchronized again, otherwise it will form a loop; An exception to the second rule above is that when a new storage server is added, the existing storage server synchronizes all the existing data (including source data and backup data) to the new server
- Add fastdfs dependency to project
<!--FastDFS rely on--> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.29-SNAPSHOT</version> </dependency>
-
Add the configuration file fastdfs to the resource directory_ client. conf
tracker_ The server points to its own server
#connection timed out connect_timeout = 2 #Network Timeout network_timeout = 30 #Coding format charset = UTF-8 #tracker port number http.tracker_http_port = 8080 #Anti theft chain function http.anti_steal_token = no #Secret key http.secret_key = FastDFS1234567890 #tracker ip: port number tracker_server = 47.93.242.4:22122 #Connection pool configuration connection_pool.enabled = true connection_pool.max_count_per_entry = 500 connection_pool.max_idle_time = 3600 connection_pool.max_wait_time_in_ms = 1000
-
Write fastdfs tool class
package com.zlq.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; /** * @ProjectName:yeb * @Package:com.zlq.server.utils * @ClassName: FastDfsUtils * @description: FastDfs Tool class * @author: LiQun * @CreateDate:2021/5/8 9:33 afternoon */ public class FastDfsUtils { private static Logger LOGGER = LoggerFactory.getLogger(FastDfsUtils.class); /** * Initialize client * ClientGlobal.init(filePath)Read the configuration file and initialize the properties */ static { try { String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath(); ClientGlobal.init(filePath); } catch (Exception e) { LOGGER.error("initialization FastDFS fail==========>", e.getMessage()); } } /** * Generate a tracker to the server * * @return * @throws IOException */ private static TrackerServer trackerServer() throws IOException { return new TrackerClient().getTrackerServer(); } /** * Generate storage client * * @return * @throws IOException */ private static StorageClient getStorageClient() throws IOException { return new StorageClient(trackerServer(), null); } /** * Upload file * * @param file * @return */ public static String[] upload(MultipartFile file) { String filename = file.getOriginalFilename(); String[] uploadResults = null; LOGGER.info("File name:" + filename); StorageClient storageClient = null; try { //Get storage client storageClient = getStorageClient(); uploadResults = storageClient.upload_file(file.getBytes(), filename.substring(filename.lastIndexOf(".") + 1), null); } catch (Exception e) { LOGGER.error("Failed to upload file===========>", e.getMessage()); } if (null == uploadResults) { LOGGER.error("Upload failed", storageClient.getErrorCode()); } return uploadResults; } /** * Get file information * * @param groupName * @param remoteFileName * @return */ public static FileInfo getFileInfo(String groupName, String remoteFileName) { StorageClient storageClient = null; try { storageClient = getStorageClient(); return storageClient.get_file_info(groupName, remoteFileName); } catch (Exception e) { LOGGER.error("File information acquisition failed" + e.getMessage()); } return null; } /** * Function Description: < br > * 〈File download * * @param groupName * @param remoteFileName * @Author: Larry * @Date: 2021/5/8 10:04 afternoon * @return: java.io.InputStream */ public static InputStream downFile(String groupName, String remoteFileName) { StorageClient storageClient = null; try { storageClient = getStorageClient(); byte[] fileByte = storageClient.download_file(groupName, remoteFileName); InputStream inputStream = new ByteArrayInputStream(fileByte); return inputStream; } catch (Exception e) { LOGGER.error("File download failed" + e.getMessage()); } return null; } /** * Delete file * * @param groupName * @param remoteFileName */ public static void deleteFile(String groupName, String remoteFileName) { StorageClient storageClient = null; try { storageClient = getStorageClient(); storageClient.delete_file(groupName, remoteFileName); } catch (Exception e) { LOGGER.error("File deletion failed" + e.getMessage()); } } /** * Get the full path of the file * * @return */ public static String getTrackerUrl() { TrackerClient trackerClient = null; TrackerServer trackerServer = null; StorageServer storeStorage = null; try { trackerClient = new TrackerClient(); trackerServer = trackerClient.getTrackerServer(); storeStorage = trackerClient.getStoreStorage(trackerServer); } catch (Exception e) { LOGGER.error("File path acquisition failed" + e.getMessage()); } return "http://" + storeStorage.getInetSocketAddress().getHostString() + ":8889/"; } }
-
Business logic
AdminService
@Override public RespBean updateAdminUserFace(String url, Integer id, Authentication authentication) { Admin admin = adminMapper.selectById(id); admin.setUserFace(url); if (1 == adminMapper.updateById(admin)){ //Get admin from permissions Admin principal = (Admin) authentication.getPrincipal(); principal.setUserFace(url); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(admin,null,authentication.getAuthorities()) ); return RespBean.success("Update succeeded!",url); } return RespBean.error("Update failed!"); }
AdminController
@ApiOperation(value = "Update user Avatar") @PostMapping("/admin/userface") public RespBean updateUserFace(MultipartFile file,Integer id,Authentication authentication){ String[] filePath = FastDfsUtils.upload(file); String url = FastDfsUtils.getTrackerUrl() + filePath[0] + "/" + filePath[1]; return adminService.updateAdminUserFace(url,id,authentication); }
webSocket chat function
WebSocket is a protocol for full duplex communication on a single TCP connection provided by HTML5. WebSocket makes the data exchange between the client and the server easier, and allows the server to actively push data to the client.
In the WebSocket API, the browser and server only need to complete a handshake, and they can directly create a persistent connection and conduct two-way data transmission, which greatly reduces the overhead. Its biggest feature is that the server can actively push information to the client, and the client can also actively send information to the server. It is a real two-way equal dialogue, which belongs to a kind of server push technology.
w
Why do I need WebSocket?
For example, if we want to know the weather today, the client can only send a request to the server and the server returns the query results. The HTTP protocol can't do this. The server actively pushes information to the client.
Now, in order to implement push technology, many websites use Ajax polling. Polling is that the browser sends an HTTP request to the server at a specific time interval (such as every 1 second), and then the server returns the latest data to the browser of the client. This traditional mode brings obvious disadvantages, that is, the browser needs to constantly send requests to the server. After all, the server cannot actively push messages to the browser in the HTTP protocol. However, the HTTP request may contain a long header, in which the really effective data may be only a small part. Obviously, this will waste a lot of bandwidth and other resources.
- Add dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
- Configuration class
package com.zlq.server.config;import com.baomidou.mybatisplus.core.toolkit.StringUtils;import com.zlq.server.config.security.component.JwtTokenUtil;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.web.socket.config.annotation.EnableWebSocketMessageBroker;import org.springframework.web.socket.config.annotation.StompEndpointRegistry;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;import javax.annotation.Resource;@Configuration@EnableWebSocketMessageBroker //Enable websocketpublic class websocketconfig implements websocketmessagebrokerconfigurer {@ value ("${JWT. Tokenhead}") private string tokenhead@ Resource private JwtTokenUtil jwtTokenUtil; @ Resource private UserDetailsService userDetailsService; /** * Add this Endpoint, so that the web page can be connected to the service through websocket * that is, we configure the service address of websocket, In addition, you can specify whether to use socketJS * * @ param registry * / @ override public void registerstampendpoints (stopendpoint Registry) {/ / register the ws/ep path as the Endpoint of stomp. Users can conduct websocket communication after connecting to this Endpoint. socketJS registry. Addendpoint ("/ ws/ep") is supported //: allow cross domain setAllowedOriginPatterns("*") / / supports socket JS access withSockJS(); } /** * Input parameter channel setting * * @ param registration * / * because we use spring security, we need to pass in the token through the input pipeline for corresponding verification, Otherwise, websocket cannot be performed, and security configuration needs to be assured. Websocket * / @ override public void configureclientinboundchannel (channelregistration) {registration. Interceptors (New channelinterceptor() {@ override public message <? > presend (message <? > message, messagechannel) {stopheaderaccessor accessor = messageheaderaccessor.getaccessor (message, stopheaderaccessor. Class); / / judge whether it is a connection. If yes, you need to get a token and set the user object if (stopcommand. Connect. Equals (accessor. Getcommand())) {string token = accessor. Getfirstnativeheader ("auth token"); if (stringutils. Isnotblank (token)) {string authToken = token. Substring (tokenhead. Length()); / / get the user name from the token string username = jwttokenutil. Getusernamefromtoken (authToken); // The user name if (StringUtils.isNotBlank(username)) {/ / log in to UserDetails userDetails = userDetailsService.loadUserByUsername(username); / / verify whether the token is valid and reset the user object if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); accessor.setUser(authenticationToken); } } } } return message; } }); } /** * Function Description: < br > * "configure message broker" * * @ param registry * @ Author: Larry * @ date: 2021 / 5 / 8 2:40 PM * @ return: void * / @ override public void configuremessagebroker (message broker Registry) {/ / configure the proxy domain. You can configure multiple proxy domains. Configure the proxy destination prefix as / queue. You can push the message registry.enableSimpleBroker("/queue") to the client on the configuration domain;}}
Note:
- Define message pojo classes
package com.zlq.server.pojo;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.experimental.Accessors;import java.time.LocalDateTime;/** * @ProjectName:yeb * @Package:com.zlq.server.pojo * @ClassName: ChatMsg * @description: * @author: LiQun * @CreateDate:2021/5/8 3:00 afternoon */@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)public class ChatMsg { private String from; private String to; private String content; private LocalDateTime date; private String fromNickName;}
-
WsController
@Controllerpublic class WsController { @Resource private SimpMessagingTemplate simpMessagingTemplate; @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()); /** * Send message * 1 Message recipient * 2 Message queue * 3 Message object */ simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(), "/queue/chat", chatMsg); }}
-
ChatController
@RestController@RequestMapping("/chat")public class ChatController { @Resource private IAdminService adminService; @ApiOperation(value = "Get all operators") @GetMapping("/admin") public List<Admin> getAllEmployee(String keyword){ return adminService.listAdmin(keyword); }}
-
Release the webSocket resource in the SpringSecurity configuration class
-