基于Vue+SpringCloudAlibaba微服务电商项目实战-构建会员服务-005:令牌登陆&扫码关注&细分接口安全领域

1 会员、令牌登陆服务接口的演示

今日课程任务

  1. 如何细分rpc接口需要细分参数安全领域
  2. 为什么要dto、do之间实现互转
  3. 构建会员服务用户注册的接口
  4. 构建会员服务令牌登录接口
  5. 基于多线程的形式异步写入登录日志
  6. @Async注解的失效之谜分析
  7. 基于令牌查询用户的信息实现用户脱敏

2 为什么我们接口需要定义dto与do转换

构建会员服务接口
数据库表设计

CREATE TABLE `meite_user` (
  `USER_ID` int(12) NOT NULL AUTO_INCREMENT COMMENT 'user_id',
  `MOBILE` varchar(11) NOT NULL COMMENT '手机号',
  `EMAIL` varchar(50) DEFAULT NULL COMMENT '邮箱号',
  `PASSWORD` varchar(64) NOT NULL COMMENT '密码',
  `USER_NAME` varchar(50) DEFAULT NULL COMMENT '用户名',
  `SEX` tinyint(1) DEFAULT '0' COMMENT '性别  1男  2女',
  `AGE` tinyint(3) DEFAULT '0' COMMENT '年龄',
  `CREATE_TIME` timestamp NULL DEFAULT NULL COMMENT '注册时间',
  `UPDATE_TIME` timestamp NULL DEFAULT NULL COMMENT '修改时间',
  `IS_AVAILABLE` tinyint(1) DEFAULT '1' COMMENT '是否可用 1正常  2冻结',
  `PIC_IMG` varchar(255) DEFAULT NULL COMMENT '用户头像',
  `QQ_OPENID` varchar(50) DEFAULT NULL COMMENT 'QQ联合登陆id',
  `WX_OPENID` varchar(50) DEFAULT NULL COMMENT '微信公众号关注id',
  PRIMARY KEY (`USER_ID`),
  UNIQUE KEY `MOBILE_UNIQUE` (`MOBILE`),
  UNIQUE KEY `EMAIL_UNIQUE` (`EMAIL`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8 COMMENT='用户会员表';

CREATE TABLE `user_login_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `login_ip` varchar(255) DEFAULT NULL,
  `login_time` datetime DEFAULT NULL,
  `login_token` varchar(255) DEFAULT NULL,
  `channel` varchar(255) DEFAULT NULL,
  `equipment` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET="utf8";

细分接口安全领域
为什么接口需要细分接口安全领域,在rpc远程调用中直接传递do参数,容易造成数据库攻击。

DO(Data Object):此对象与数据库表结构一一对应,通过DAO层向上传输数据源对象。
DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
BO(Business Object):业务对象,由Service层输出的封装业务逻辑的对象。
AO(Application Object):应用对象,在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类。
参考阿里云官方开发手册。

用户会员注册如果采用数据库层实体类接受和响应参数,会暴露参数、容易被人参数攻击,不安全
RPC通讯中接收与响应参数封装,使用dto对象

3 定义会员注册接口的dto参数

UserDO&UserReqRegisterDTO

@Data
public class UserDO {

    /**
     * userid
     */

    private Long userId;
    /**
     * 手机号码
     */

    private String mobile;
    /**
     * 邮箱
     */

    private String email;
    /**
     * 密码
     */

    private String password;
    /**
     * 用户名称
     */

    private String userName;
    /**
     * 性别 0 男 1女
     */

    private char sex;
    /**
     * 年龄
     */

    private Long age;
    /**
     * 注册时间
     */

    private Date createTime;
    /**
     * 修改时间
     *
     */

    private Date updateTime;
    /**
     * 账号是否可以用 1 正常 0冻结
     */

    private char isAvailable;
    /**
     * 用户头像
     */

    private String picImg;
    /**
     * 用户关联 QQ 开放ID
     */

    private String qqOpenid;
    /**
     * 用户关联 微信 开放ID
     */
    private String wxOpenId;

}
@Data
public class UserReqRegisterDTO {

    /**
     * 手机号码
     */
    @ApiModelProperty(value = "手机号码", name = "mobile", required = true)
    private String mobile;

    /**
     * 密码
     */
    @ApiModelProperty(value = "密码", name = "password", required = true)
    private String password;

//    private String smsCode;
}

4 定义dto与do的转换工具类封装

DTO转换DO、DO转换DTO工具类

public class MeiteBeanUtils<Dto, Do> {

    /**
     * dot 转换为Do 工具类
     */
    public static <Do> Do dtoToDo(Object dtoEntity, Class<Do> doClass) {
        // 判断dto是否为空!
        if (dtoEntity == null) {
            return null;
        }
        // 判断DoClass 是否为空
        if (doClass == null) {
            return null;
        }
        try {
            Do newInstance = doClass.newInstance();
            BeanUtils.copyProperties(dtoEntity, newInstance);
            // Dto转换Do
            return newInstance;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * do 转换为Dto 工具类
     */
    public static <Dto> Dto doToDto(Object doEntity, Class<Dto> dtoClass) {
        // 判断dto是否为空!
        if (doEntity == null) {
            return null;
        }
        // 判断DoClass 是否为空
        if (dtoClass == null) {
            return null;
        }
        try {
            Dto newInstance = dtoClass.newInstance();
            BeanUtils.copyProperties(doEntity, newInstance);
            // Dto转换Do
            return newInstance;
        } catch (Exception e) {
            return null;
        }
    }
}

封装进BaseApiService中

@Data
public class BaseApiService<T> {

    public BaseResponse<T> setResultError(Integer code, String msg) {
        return setResult(code, msg, null);
    }

    /**
     * 返回错误,可以传msg
     *
     * @param msg
     * @return
     */
    public BaseResponse<T> setResultError(String msg) {
        return setResult(Constants.HTTP_RES_CODE_500, msg, null);
    }

    /***
     * 返回成功,可以传data值
     * @param data
     * @return
     */
    public BaseResponse<T> setResultSuccess(T data) {
        return setResult(Constants.HTTP_RES_CODE_200, Constants.HTTP_RES_CODE_200_VALUE, data);
    }

    /**
     * 返回成功,沒有data值
     *
     * @return
     */
    public BaseResponse<T> setResultSuccess() {
        return setResult(Constants.HTTP_RES_CODE_200, Constants.HTTP_RES_CODE_200_VALUE, null);
    }


    /**
     * 通用封装 通用封装
     *
     * @param code
     * @param msg
     * @param data
     * @return
     */

    public BaseResponse<T> setResult(Integer code, String msg, T data) {
        return new BaseResponse<T>(code, msg, data);
    }

    /**
     * dto转换do
     *
     * @param dtoEntity
     * @param doClass
     * @param <Do>
     * @return
     */
    public static <Do> Do dtoToDo(Object dtoEntity, Class<Do> doClass) {
        return MeiteBeanUtils.dtoToDo(dtoEntity, doClass);
    }

    /**
     * do转dto
     * @param doEntity
     * @param dtoClass
     * @param <Dto>
     * @return
     */
    public static <Dto> Dto doToDto(Object doEntity, Class<Dto> dtoClass) {
        return MeiteBeanUtils.doToDto(doEntity, dtoClass);
    }

    public BaseResponse<T> setResult(int dbCount, T successMsg, String errorMsg) {
        return dbCount > 0 ? setResultSuccess(successMsg) : setResultError(errorMsg);
    }
}

5 会员服务注册接口测试运行

public interface UserMapper {

    @Insert("INSERT INTO `meite_user` VALUES (null, #{mobile},null, #{password}, null, '0', '0', now()," +
            " now(),'1',null , null, null);\n")
    int register(UserDo userDo);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",user_name as username ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime,IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
            "\n" +
            "from meite_user  where MOBILE=#{mobile}")
    UserDo login(String mobile, String passWord);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",user_name as username ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime,IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
            "\n" +
            "from meite_user  where MOBILE=#{mobile}")
    UserDo existMobile(String mobile);
    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",user_name as username ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime,IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
            "\n" +
            "from meite_user  where USER_ID=#{userId}")
    UserDo findByUser(Long userId);
}
public class MD5Util {

   public final static String MD5(String s) {
      char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
      try {
         byte[] btInput = s.getBytes();
         // 获得MD5摘要算法的 MessageDigest 对象
         MessageDigest mdInst = MessageDigest.getInstance("MD5");
         // 使用指定的字节更新摘要
         mdInst.update(btInput);
         // 获得密文
         byte[] md = mdInst.digest();
         // 把密文转换成十六进制的字符串形式
         int j = md.length;
         char str[] = new char[j * 2];
         int k = 0;
         for (int i = 0; i < j; i++) {
            byte byte0 = md[i];
            str[k++] = hexDigits[byte0 >>> 4 & 0xf];
            str[k++] = hexDigits[byte0 & 0xf];
         }
         return new String(str);
      } catch (Exception e) {
         e.printStackTrace();
         return null;
      }
   }
}
@Api(tags = "会员注册服务接口")
@ApiModel
public interface MemberRegisterService {

    /**
     * 会员注册接口
     * @param userReqRegisterDto
     * @return
     */
    @PostMapping("/register")
    BaseResponse<JSONObject> register(@RequestBody UserReqRegisterDTO userReqRegisterDTO);
}
@RestController
public class MemberRegisterServiceImpl extends BaseApiService implements MemberRegisterService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public BaseResponse<JSONObject> register(UserReqRegisterDTO userReqRegisterDto) {
        // 参数验证
        String mobile = userReqRegisterDto.getMobile();
        if (StringUtils.isEmpty(mobile)) {
            return setResultError("mobile参数不能为空");
        }
        String password = userReqRegisterDto.getPassword();
        if (StringUtils.isEmpty(password)) {
            return setResultError("password参数不能为空");
        }
        // 先检查手机号码是否存在
        UserDO userDbDo = userMapper.existMobile(mobile);
        if (userDbDo != null) {
            return setResultError("该手机号码已经存在");
        }
        // dto转换成vo
        UserDO userDo = dtoToDo(userReqRegisterDto, UserDO.class);
        String newPassword = MD5Util.MD5(password + "salt");
        // 加盐 提前定义一个常量 可以用一张表定义每个用户对应的盐值
        userDo.setPassword(newPassword);
        int register = userMapper.register(userDo);
//        return register > 0 ? setResultSuccess("注册成功") : setResultError("注册失败");
        return setResult(register, "注册成功", "注册失败");
    }
}

测试效果:
在这里插入图片描述

6 令牌登录接口的基本实现

前端vue如何绑定会话信息?
前端vue使用ajax调用后端登录接口,获取令牌存放到缓存中,每次调用其他接口的时候都会传递该令牌。

登录接口:登录成功之后返回令牌给客户端保存,客户端后期使用该令牌绑定会话信息。
令牌:类似于sessionId,一般存放在redis中解决session共享问题
令牌 通行证 有效期 生成临时且唯一
Redis.set(“userToken”,”userId”);

TokenUtil

@Component
public class TokenUtil {

    @Autowired
    private RedisUtil redisUtil;

    public String createToken(String prefix, String value, Long timeOut) {
        // 1.生成令牌
        String token = prefix + "_" + UUID.randomUUID().toString().replace("-", "");
        // 2.将该token存入到redis中
        redisUtil.setString(token, value, timeOut);
        return token;
    }

    public String createToken(String prefix, String value) {
        return createToken(prefix, value, null);
    }

    public String getTokenValue(String token) {
        return redisUtil.getString(token);
    }
}

RedisUtil

@Component
public class RedisUtil {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 存放string类型
     *
     * @param key     key
     * @param data    数据
     * @param timeout 超时间
     */
    public void setString(String key, String data, Long timeout) {
        stringRedisTemplate.opsForValue().set(key, data);
        if (timeout != null) {
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    /**
     * 存放string类型
     *
     * @param key  key
     * @param data 数据
     */
    public void setString(String key, String data) {
        setString(key, data, null);
    }

    /**
     * 根据key查询string类型
     *
     * @param key
     * @return
     */
    public String getString(String key) {
        String value = stringRedisTemplate.opsForValue().get(key);
        return value;
    }

    /**
     * 根据对应的key删除key
     *
     * @param key
     */
    public void delKey(String key) {
        stringRedisTemplate.delete(key);
    }
}

引入redis相关maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

登录接口及实现类

@Data
public class UserLoginDTO {

    /**
     * 手机号码
     */
    @ApiModelProperty(value = "手机号码", name = "mobile", required = true)
    private String mobile;

    /**
     * 密码
     */
    @ApiModelProperty(value = "密码", name = "password", required = true)
    private String password;

}
@Api(tags = "会员登录接口")
public interface MemberLoginService {

    @PostMapping("/login")
    BaseResponse<JSONObject> login(@RequestBody UserLoginDTO userLoginDTO);
}
@RestController
public class MemberLoginServiceImpl extends BaseApiService implements MemberLoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TokenUtil tokenUtil;

    @Value("${mayikt.login.token.prefix}")
    private String loginTokenPrefix;

    @Override
    public BaseResponse<JSONObject> login(UserLoginDTO userLoginDTO) {
        // 参数验证
        String mobile = userLoginDTO.getMobile();
        if (StringUtils.isEmpty(mobile)) {
            return setResultError("mobile参数不能为空");
        }
        String password = userLoginDTO.getPassword();
        if (StringUtils.isEmpty(password)) {
            return setResultError("password参数不能为空");
        }
        // 查询数据库
        String newPassword = MD5Util.MD5(password);
        UserDO loginUserDO = userMapper.login(mobile, newPassword);
        if (loginUserDO == null) {
            return setResultError("手机号码或者密码不正确");
        }
        Long userId = loginUserDO.getUserId();
        String userToken = tokenUtil.createToken(loginTokenPrefix, userId + "");
        JSONObject resultJSON = new JSONObject();
        resultJSON.put("userToken", userToken);
        return setResultSuccess(resultJSON);
    }
}

测试效果:
在这里插入图片描述

7 基于令牌获取用户信息接口

字段脱敏工具类DesensitizationUtil

public class DesensitizationUtil {

    /**
     * 只显示第一个汉字,其他隐藏为2个星号<例子:李**>
     *
     * @param fullName
     * @param index    1 为第index位开始脱敏
     * @return
     */
    public static String left(String fullName, int index) {
        if (StringUtils.isBlank(fullName)) {
            return "";
        }
        String name = StringUtils.left(fullName, index);
        return StringUtils.rightPad(name, StringUtils.length(fullName), "*");
    }

    /**
     * 110****58,前面保留3位明文,后面保留2位明文
     *
     * @param name
     * @param index 3
     * @param end   2
     * @return
     */
    public static String around(String name, int index, int end) {
        if (StringUtils.isBlank(name)) {
            return "";
        }
        return StringUtils.left(name, index).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(name, end), StringUtils.length(name), "*"), "***"));
    }

    /**
     * 后四位,其他隐藏<例子:****1234>
     *
     * @param num
     * @return
     */
    public static String right(String num, int end) {
        if (StringUtils.isBlank(num)) {
            return "";
        }
        return StringUtils.leftPad(StringUtils.right(num, end), StringUtils.length(num), "*");
    }

    // 手机号码前三后四脱敏
    public static String mobileEncrypt(String mobile) {
        if (StringUtils.isEmpty(mobile) || (mobile.length() != 11)) {
            return mobile;
        }
        return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

    //身份证前三后四脱敏
    public static String idEncrypt(String id) {
        if (StringUtils.isEmpty(id) || (id.length() < 8)) {
            return id;
        }
        return id.replaceAll("(?<=\\w{3})\\w(?=\\w{4})", "*");
    }

    //护照前2后3位脱敏,护照一般为8或9位
    public static String idPassport(String id) {
        if (StringUtils.isEmpty(id) || (id.length() < 8)) {
            return id;
        }
        return id.substring(0, 2) + new String(new char[id.length() - 5]).replace("\0", "*") + id.substring(id.length() - 3);
    }

    /**
     * 证件后几位脱敏
     *
     * @param id
     * @param sensitiveSize
     * @return
     */
    public static String idPassport(String id, int sensitiveSize) {
        if (StringUtils.isBlank(id)) {
            return "";
        }
        int length = StringUtils.length(id);
        return StringUtils.rightPad(StringUtils.left(id, length - sensitiveSize), length, "*");
    }
}

根据token查看用户信息接口&实现

@Api(tags = "用户会员信息基本接口")
public interface MemberInfoService {

    /**
     * 根据用户token查询用户信息
     *
     * @param token
     * @return
     */
    @GetMapping("/getTokenUser")
    @ApiOperation("根据token查看用户信息")
    @ApiImplicitParam(name = "token", value = "token", required = true)
    BaseResponse<UserRespDTO> getTokenUser(@RequestParam("token") String token);
}
@RestController
public class MemberInfoServiceImpl extends BaseApiService implements MemberInfoService {

    @Autowired
    private TokenUtil tokenUtil;
    @Autowired
    private UserMapper userMapper;

    @Override
    public BaseResponse<UserRespDTO> getTokenUser(String token) {

        if (StringUtils.isEmpty(token)) {
            return setResultError("token不能为空");
        }
        // 从Redis中获取到userId
        String redisValue = tokenUtil.getTokenValue(token);
        if (StringUtils.isEmpty(redisValue)) {
            return setResultError("token已经过期");
        }
        long userId = Long.parseLong(redisValue);
        // 根据userId查询用户信息
        UserDO userDO = userMapper.findByUser(userId);
        if (userDO == null) {
            return setResultError("token已经过期或者错误!");
        }
        UserRespDTO userRespDTO = doToDto(userDO, UserRespDTO.class);
        String mobile = userRespDTO.getMobile();
        userRespDTO.setMobile(DesensitizationUtil.mobileEncrypt(mobile));
        return setResultSuccess(userRespDTO);
    }
}

测试效果:
在这里插入图片描述

8 使用多线程异步的形式处理日志操作

为了能够提高登录接口的响应速度,应该采用异步的形式写入日志
注意:单独开启线程的业务逻辑@Async不应该放在接口实现类里面,容易造成冲突。
启动类加上注解@EnableAsync开启注解权限

实体类UserLoginLogDo

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginLogDo {
    Long id;
    Long userId;
    String loginIp;
    Date loginTime;
    String loginToken;
    String channel;
    String equipment;

    public UserLoginLogDo(Long userId, String loginIp, Date loginTime, String loginToken, String channel, String equipment) {
        this.userId = userId;
        this.loginIp = loginIp;
        this.loginTime = loginTime;
        this.loginToken = loginToken;
        this.channel = channel;
        this.equipment = equipment;
    }

    @Override
    public String toString() {
        return "UserLoginLogDo{" +
                "userId=" + userId +
                ", loginIp='" + loginIp + '\'' +
                ", loginTime=" + loginTime +
                ", loginToken='" + loginToken + '\'' +
                ", channel='" + channel + '\'' +
                ", equipment='" + equipment + '\'' +
                '}';
    }
}

UserLoginLogMapper

public interface UserLoginLogMapper {


    @Insert("\n" +
            "insert into  user_login_log values(null,#{userId},#{loginIp},now(),#{loginToken},#{channel},#{equipment});\n")
    int insertUserLoginLog(UserLoginLogDo userLoginLogDo);
}

登录实现类MemberLoginServiceImpl加上log日志

@RestController
@Slf4j
public class MemberLoginServiceImpl extends BaseApiService implements MemberLoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TokenUtil tokenUtil;

    @Value("${mayikt.login.token.prefix}")
    private String loginTokenPrefix;

    @Autowired
    private AsyncLoginLogManage asyncLoginLogManage;

    @Override
    public BaseResponse<JSONObject> login(UserLoginDTO userLoginDTO) {
        // 参数验证
        String mobile = userLoginDTO.getMobile();
        if (StringUtils.isEmpty(mobile)) {
            return setResultError("mobile参数不能为空");
        }
        String password = userLoginDTO.getPassword();
        if (StringUtils.isEmpty(password)) {
            return setResultError("password参数不能为空");
        }
        // 查询数据库
        String newPassword = MD5Util.MD5(password);
        UserDO loginUserDO = userMapper.login(mobile, newPassword);
        if (loginUserDO == null) {
            return setResultError("手机号码或者密码不正确");
        }
        Long userId = loginUserDO.getUserId();
        String userToken = tokenUtil.createToken(loginTokenPrefix, userId + "");
        JSONObject resultJSON = new JSONObject();
        resultJSON.put("userToken", userToken);

        // 写入日志
        log.info(Thread.currentThread().getName() + " 处理流程1");
        asyncLoginLogManage.loginLog(userId, "192.168.212.110", new Date(), userToken
                , "PC", "windows 谷歌浏览器");
        log.info(Thread.currentThread().getName() + " 处理流程3");
        return setResultSuccess(resultJSON);
    }
}

AsyncLoginLogManage开启异步日志记录

@Component
@Slf4j
public class AsyncLoginLogManage {

    @Autowired
    private UserLoginLogMapper userLoginLogMapper;

    @Async
    public void loginLog(Long userId, String loginIp, Date loginTime, String loginToken, String channel,
                         String equipment) {
        UserLoginLogDo userLoginLogDo = new UserLoginLogDo(userId, loginIp, loginTime, loginToken, channel, equipment);
        log.info(Thread.currentThread().getName() + ",userLoginLogDo:" + userLoginLogDo.toString() + ",流程2");
        userLoginLogMapper.insertUserLoginLog(userLoginLogDo);
        log.info(Thread.currentThread().getName() + " 处理流程2");
    }
}

测试效果:
在这里插入图片描述

9 @Async整合可能存在的问题

直接在实现类中方法上加上@Async注解,可能导致两个问题

  1. @Async导致SpringMVC请求404(接口实现类)
  2. @Async不生效

加入@Async后,该接口没有注册到SpringMVC中
建议最好使用单独一个类调用

@Async整合线程池的配置

@Configuration
public class MayiktTaskExecutorConfig implements AsyncConfigurer {

    /**
     * 设置ThreadPoolExecutor的核心池大小。
     */
    private static final int CORE_POOL_SIZE = 2;
    /**
     * 设置ThreadPoolExecutor的最大池大小。
     */
    private static final int MAX_POOL_SIZE = 4;
    /**
     * 设置ThreadPoolExecutor的BlockingQueue的容量。
     */
    private static final int QUEUE_CAPACITY = 10;

    /**
     * 通过重写getAsyncExecutor方法,制定默认的任务执行由该方法产生
     * <p>
     * 配置类实现AsyncConfigurer接口并重写getAsyncExcutor方法,并返回一个ThreadPoolTaskExevutor
     * 这样我们就获得了一个基于线程池的TaskExecutor
     */
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
        taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
        taskExecutor.initialize();
        return taskExecutor;
    }
}

MayiktLoginServiceImpl

@RestController
public class MayiktLoginServiceImpl {

    @GetMapping("myLogin")
    public String myLogin() {
        System.out.println(">>myLogin threadName:" + Thread.currentThread().getName());
        myLoginService();
        return "myLogin";
    }

    @Async
    public void myLoginService() {
        System.out.println(">>myLoginService threadName:" + Thread.currentThread().getName());
    }
}

测试效果:
在这里插入图片描述
@Async导致404或者失效问题总结:

  1. 如果控制类实现了接口,在使用@Async异步注解的时候会导致当前的控制类所有注册的url映射全部为404;
  2. 如果控制类没有实现接口,直接使用@Async异步注解,会失效

自定义注解如何能够生效:AOP技术

原理分析:
当在方法上加上用@Async的时候,就会对当前这个类生成一个代理类,必须通过代理类.myLoginService才能走Aop切面帮助创建线程进行处理。

10 源码角度分析@Async为什么会失效

@Component
public class SpringBeanContext implements ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(SpringBeanContext.class);
    protected static ApplicationContext applicationContext;

    public SpringBeanContext() {
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext1) throws BeansException {
        applicationContext = applicationContext1;
    }

    public static Object getBean(String beanName) {
        if (applicationContext == null) {
            log.error("未初始化Spring上下文");
            return null;
        } else if (!applicationContext.containsBean(beanName)) {
            log.warn("Spring上下文中不存在要查找的对象[{}]", beanName);
            return null;
        } else {
            return applicationContext.getBean(beanName);
        }
    }

    public static <T> T getBean(Class<T> clazz) {
        if (applicationContext == null) {
            log.error("未初始化Spring上下文");
            return null;
        } else {
            return applicationContext.getBean(clazz);
        }
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        if (applicationContext == null) {
            log.error("未初始化Spring上下文");
            return null;
        } else {
            return applicationContext.getBean(name, clazz);
        }
    }

    public String[] getBeanNamesForType(Class<?> type) {
        return applicationContext.getBeanNamesForType(type);
    }
}
@RestController
public class MayiktLoginServiceImpl {

    @GetMapping("myLogin")
    public String myLogin() {
        System.out.println(">>myLogin threadName:" + Thread.currentThread().getName());
        // 获取cglib方式生成的代理类对象
        MayiktLoginServiceImpl mayiktLoginServiceImpl = (MayiktLoginServiceImpl) SpringBeanContext.getBean("mayiktLoginServiceImpl");
        mayiktLoginServiceImpl.myLoginService();
        return "myLogin";
    }

    @Async
    public void myLoginService() {
        System.out.println(">>myLoginService threadName:" + Thread.currentThread().getName());
    }
}

测试效果:
在这里插入图片描述
如果控制类没有实现过接口的情况下,采用cglib动态代理,可以成功的注册到SpringMVC的容器中;
如果控制类有实现接口,走jdk动态代理技术,无法将该动态代理类注册到SpringMVC容器中。(相当于@Controll注解和@Async注解生成代理类产生冲突)

自理解:方法上加上@Async异步注解,会采用动态代理方式创建代理类。如果实现过接口,采用jdk动态代理生成的代理类不符合SpringMVC控制类类型,不能注册到SpringMVC中;如果没有实现接口,采用cglib动态代理生成代理类,不会改变控制类性质,同样可以注册SpringMVC容器中,但是执行异步方法myLoginService();的时候要先获取代理类对象执行才能生效,否则无效。

为什么加上@Async会导致控制类中定义的Mapping为404?
AbstractHandlerMethodMapping查看到afterPropertiesSet方法中initHandlerMethods()关联SpringMVCBean。

11 @Async需要注意那些事项

使用@Async注意事项:

  1. 异步需要执行的业务逻辑建议单独开一个类实现或者从容器中直接获取到该代理类执行;
  2. 异步的方法上不要加上static(不会走aop);