使用责任链设计模式修改登录接口的业务代码,提高扩展性

先看一下优化之前的登录代码:UserServiceImpl.java

package cn.edu.sgu.www.mhxysy.service.system.impl;

import cn.edu.sgu.www.mhxysy.consts.RedisKeyPrefixConst;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.entity.system.UserLoginLog;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.property.EmailProperties;
import cn.edu.sgu.www.mhxysy.property.SystemSettingsProperties;
import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import cn.edu.sgu.www.mhxysy.redis.StringRedisUtils;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import cn.edu.sgu.www.mhxysy.service.system.UserService;
import cn.edu.sgu.www.mhxysy.util.IpUtils;
import cn.edu.sgu.www.mhxysy.util.StringUtils;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

/**
 * @author heyunlin
 * @version 1.0
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

	private final FeignService feignService;
	private final JavaMailSender javaMailSender;
	private final EmailProperties emailProperties;
	private final RedisRepository redisRepository;
	private final StringRedisUtils stringRedisUtils;
	private final SystemSettingsProperties systemSettingsProperties;

	@Autowired
	public UserServiceImpl(
			FeignService feignService,
			JavaMailSender javaMailSender,
			EmailProperties emailProperties,
			RedisRepository redisRepository,
			StringRedisUtils stringRedisUtils,
			SystemSettingsProperties systemSettingsProperties) {
		this.feignService = feignService;
		this.javaMailSender = javaMailSender;
		this.emailProperties = emailProperties;
		this.redisRepository = redisRepository;
		this.stringRedisUtils = stringRedisUtils;
		this.systemSettingsProperties = systemSettingsProperties;
	}
	
	@Override
	public void login(UserLoginDTO loginDTO) {
		// 一、验证码判断
		// 得到用户输入的验证码
		String code = loginDTO.getCode();

		// 获取正确的验证码
		String uuid = loginDTO.getUuid();
		String key = RedisKeyPrefixConst.PREFIX_CAPTCHA + uuid;
		String realCode = stringRedisUtils.get(key);

		// 得到的验证码为空,则获取验证码到登录之间的时间已经过了3分钟,验证码过期已经被删除
		if (realCode == null) {
			throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码已失效,请刷新页面重新获取~");
		}
		// 验证码校验
		if (!code.equalsIgnoreCase(realCode)) {
			throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码错误~");
		}

		// 二、登录流程
		// 得到用户名
		String username = loginDTO.getUsername();
		log.debug("用户{}正在登录...", username);

		// 查询用户信息,如果用户被锁定,提前退出
		User user = feignService.selectByUsername(username);

		if (user != null) {
			if (user.getEnable()) {
				// 1、shiro登录认证
				UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
				Subject subject = UserUtils.getSubject();

				subject.login(token);
				// 设置session失效时间:永不超时
				subject.getSession().setTimeout(-1001);

				// 2、修改管理员上一次登录时间
				User usr = new User();

				usr.setId(user.getId());
				usr.setLastLoginTime(LocalDateTime.now());

				feignService.updateById(usr);

				// 3、邮件通知
				if (emailProperties.isEnable()) {
					new Thread(() -> {
						// 定义日期格式
						DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");

						MimeMessage message = javaMailSender.createMimeMessage();
						MimeMessageHelper helper = new MimeMessageHelper(message);

						try {
							String text = "您的账号" + username + "在广州登录了。" +
									"[" + LocalDateTime.now(ZoneId.of(systemSettingsProperties.getZoneId())).format(formatter) + "]";

							helper.setFrom(emailProperties.getFrom());
							helper.setTo(emailProperties.getTo());
							helper.setText(text);

							javaMailSender.send(message);
						} catch (MessagingException e) {
							e.printStackTrace();
						}
					}).start();
				}

				// 4、如果开启了系统日志,添加管理员登录历史
				if (systemSettingsProperties.isLoginLog()) {
					UserLoginLog loginLog = new UserLoginLog();

					loginLog.setId(StringUtils.uuid());
					loginLog.setUserId(user.getId());
					loginLog.setLoginIp(IpUtils.getIp());
					loginLog.setLoginTime(LocalDateTime.now());
					loginLog.setLoginHostName(IpUtils.getLocalHostName());

					feignService.saveLoginLog(loginLog);
				}

				// 5、从redis中删除用户权限
				redisRepository.delete(username);

				// 6、查询用户的权限信息,并保存到redis
				redisRepository.save(username);
			} else {
				throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
			}
		} else {
			throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
		}
	}

}

因为刚好刷视频刷到了责任链模式,想着在项目上能不能应用上,刚好登录接口完美符合了使用场景。没有开启事务。

于是就开始着手设计处理链,首先创建一个接口,表示登录的处理器,里面定义了处理方法、设置参数和设置下一个处理器的方法。

起初,handle()方法上是有一个Object params参数的,后来通过整体需求,调整为将参数设置成成员变量,通过setParams()方法手动设置。然后在UserLoginHandler接口的具体实现类的handle()方法上调用下一个处理器的handler()方法(如果下一个处理器不为null)。

package cn.edu.sgu.www.mhxysy.chain.login;

/**
 * 登录处理器
 * @author heyunlin
 * @version 1.0
 */
public interface UserLoginHandler {

    void handle();

    void setNext(UserLoginHandler next);

    void setParams(Object params);
}

验证码判断的处理器CaptchaHandler.java

package cn.edu.sgu.www.mhxysy.chain.login;

import cn.edu.sgu.www.mhxysy.consts.RedisKeyPrefixConst;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.redis.StringRedisUtils;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class CaptchaHandler implements UserLoginHandler {

    private Object params;
    private UserLoginHandler next;

    private final StringRedisUtils stringRedisUtils;

    @Autowired
    public CaptchaHandler(StringRedisUtils stringRedisUtils) {
        this.stringRedisUtils = stringRedisUtils;
    }

    @Override
    public void handle() {
        UserLoginDTO loginDTO = (UserLoginDTO) params;
        // 得到用户输入的验证码
        String code = loginDTO.getCode();

        // 获取正确的验证码
        String uuid = loginDTO.getUuid();
        String key = RedisKeyPrefixConst.PREFIX_CAPTCHA + uuid;
        String realCode = stringRedisUtils.get(key);

        // 得到的验证码为空,则获取验证码到登录之间的时间已经过了3分钟,验证码过期已经被删除
        if (realCode == null) {
            throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码已失效,请刷新页面重新获取~");
        }
        // 验证码校验
        if (!code.equalsIgnoreCase(realCode)) {
            throw new GlobalException(ResponseCode.BAD_REQUEST, "验证码错误~");
        }

        if (next != null) {
            next.handle();
        }
    }

    @Override
    public void setNext(UserLoginHandler next) {
        this.next = next;
    }

    @Override
    public void setParams(Object params) {
        this.params = params;
    }

}

 负责shiro登录认证的处理器LoginHandler.java

package cn.edu.sgu.www.mhxysy.chain.login;

import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class LoginHandler implements UserLoginHandler {

    private Object params;
    private UserLoginHandler next;


    @Override
    public void handle() {
        UserLoginDTO loginDTO = (UserLoginDTO) params;

        // shiro登录认证
        UsernamePasswordToken token = new UsernamePasswordToken(loginDTO.getUsername(), loginDTO.getPassword());
        Subject subject = UserUtils.getSubject();
        subject.login(token);
        // 设置session失效时间:永不超时
        subject.getSession().setTimeout(-1001);

        if (next != null) {
            next.handle();
        }
    }

    @Override
    public void setNext(UserLoginHandler next) {
        this.next = next;
    }

    @Override
    public void setParams(Object params) {
        this.params = params;
    }

}

负责修改用户最后一次登录时间的处理器UserUpdateHandler.java

package cn.edu.sgu.www.mhxysy.chain.login;

import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class UserUpdateHandler implements UserLoginHandler {

    private Object params;
    private UserLoginHandler next;

    private final FeignService feignService;

    @Autowired
    public UserUpdateHandler(FeignService feignService) {
        this.feignService = feignService;
    }

    @Override
    public void handle() {
        String userId = (String) params;
        User usr = new User();

        usr.setId(userId);
        usr.setLastLoginTime(LocalDateTime.now());

        feignService.updateById(usr);

        if (next != null) {
            next.handle();
        }
    }

    @Override
    public void setNext(UserLoginHandler next) {
        this.next = next;
    }

    @Override
    public void setParams(Object params) {
        this.params = params;
    }

}

发送邮件通知的处理器EmailSendHandler.java

package cn.edu.sgu.www.mhxysy.chain.login;

import cn.edu.sgu.www.mhxysy.property.EmailProperties;
import cn.edu.sgu.www.mhxysy.property.SystemSettingsProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class EmailSendHandler implements UserLoginHandler {

    private Object params;
    private UserLoginHandler next;

    private final JavaMailSender javaMailSender;
    private final EmailProperties emailProperties;
    private final SystemSettingsProperties systemSettingsProperties;

    @Autowired
    public EmailSendHandler(
            JavaMailSender javaMailSender,
            EmailProperties emailProperties,
            SystemSettingsProperties systemSettingsProperties) {
        this.javaMailSender = javaMailSender;
        this.emailProperties = emailProperties;
        this.systemSettingsProperties = systemSettingsProperties;
    }

    @Override
    public void handle() {
        if (emailProperties.isEnable()) {
            new Thread(() -> {
                // 定义日期格式
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");

                MimeMessage message = javaMailSender.createMimeMessage();
                MimeMessageHelper helper = new MimeMessageHelper(message);

                try {
                    String username = (String) params;
                    String text = "您的账号" + username + "在广州登录了。" +
                            "[" + LocalDateTime.now(ZoneId.of(systemSettingsProperties.getZoneId())).format(formatter) + "]";

                    helper.setFrom(emailProperties.getFrom());
                    helper.setTo(emailProperties.getTo());
                    helper.setText(text);

                    javaMailSender.send(message);
                } catch (MessagingException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        if (next != null) {
            next.handle();
        }
    }

    @Override
    public void setNext(UserLoginHandler next) {
        this.next = next;
    }

    @Override
    public void setParams(Object params) {
        this.params = params;
    }

}

负责保存用户登录日志的处理器LoginLogHandler.java

package cn.edu.sgu.www.mhxysy.chain.login;

import cn.edu.sgu.www.mhxysy.entity.system.UserLoginLog;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.property.SystemSettingsProperties;
import cn.edu.sgu.www.mhxysy.util.IpUtils;
import cn.edu.sgu.www.mhxysy.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class LoginLogHandler implements UserLoginHandler {

    private Object params;
    private UserLoginHandler next;

    private final FeignService feignService;
    private final SystemSettingsProperties systemSettingsProperties;

    @Autowired
    public LoginLogHandler(
            FeignService feignService, SystemSettingsProperties systemSettingsProperties) {
        this.feignService = feignService;
        this.systemSettingsProperties = systemSettingsProperties;
    }

    @Override
    public void handle() {
        if (systemSettingsProperties.isLoginLog()) {
            String userId = (String) params;
            UserLoginLog loginLog = new UserLoginLog();

            loginLog.setId(StringUtils.uuid());
            loginLog.setUserId(userId);
            loginLog.setLoginIp(IpUtils.getIp());
            loginLog.setLoginTime(LocalDateTime.now());
            loginLog.setLoginHostName(IpUtils.getLocalHostName());

            feignService.saveLoginLog(loginLog);
        }

        if (next != null) {
            next.handle();
        }
    }

    @Override
    public void setNext(UserLoginHandler next) {
        this.next = next;
    }

    @Override
    public void setParams(Object params) {
        this.params = params;
    }

}

 负责更新redis缓存的处理器

package cn.edu.sgu.www.mhxysy.chain.login;

import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author heyunlin
 * @version 1.0
 */
@Component
public class RedisCacheHandler implements UserLoginHandler {

    private Object params;
    private UserLoginHandler next;

    private final RedisRepository redisRepository;

    @Autowired
    public RedisCacheHandler(RedisRepository redisRepository) {
        this.redisRepository = redisRepository;
    }

    @Override
    public void handle() {
        String username = (String) params;

        // 从redis中删除用户权限
        redisRepository.delete(username);
        // 查询用户的权限信息,并保存到redis
        redisRepository.save(username);

        if (next != null) {
            next.handle();
        }
    }

    @Override
    public void setNext(UserLoginHandler next) {
        this.next = next;
    }

    @Override
    public void setParams(Object params) {
        this.params = params;
    }

}

最后,重构后的代码

package cn.edu.sgu.www.mhxysy.service.system.impl;

import cn.edu.sgu.www.mhxysy.chain.login.UserLoginHandler;
import cn.edu.sgu.www.mhxysy.dto.system.UserLoginDTO;
import cn.edu.sgu.www.mhxysy.dto.system.UserUpdateDTO;
import cn.edu.sgu.www.mhxysy.entity.system.User;
import cn.edu.sgu.www.mhxysy.exception.GlobalException;
import cn.edu.sgu.www.mhxysy.feign.FeignService;
import cn.edu.sgu.www.mhxysy.redis.RedisRepository;
import cn.edu.sgu.www.mhxysy.restful.ResponseCode;
import cn.edu.sgu.www.mhxysy.service.system.UserService;
import cn.edu.sgu.www.mhxysy.util.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author heyunlin
 * @version 1.0
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

	private final UserLoginHandler captchaHandler;
	private final UserLoginHandler userUpdateHandler;
	private final UserLoginHandler emailSendHandler;
	private final UserLoginHandler loginLogHandler;
	private final UserLoginHandler loginHandler;
	private final UserLoginHandler redisCacheHandler;
	private final FeignService feignService;
	private final RedisRepository redisRepository;

	@Autowired
	public UserServiceImpl(
			UserLoginHandler captchaHandler,
			UserLoginHandler userUpdateHandler,
			UserLoginHandler emailSendHandler,
			UserLoginHandler loginLogHandler,
			UserLoginHandler loginHandler,
			UserLoginHandler redisCacheHandler,
			FeignService feignService,
			RedisRepository redisRepository) {
		this.captchaHandler = captchaHandler;
		this.userUpdateHandler = userUpdateHandler;
		this.emailSendHandler = emailSendHandler;
		this.loginLogHandler = loginLogHandler;
		this.loginHandler = loginHandler;
		this.redisCacheHandler = redisCacheHandler;
		this.feignService = feignService;
		this.redisRepository = redisRepository;
	}


	@Override
	public void logout() {
		// 删除角色的权限
		redisRepository.delete(UserUtils.getLoginUsername());

		// 注销
		UserUtils.getSubject().logout();
	}

	@Override
	public void login(UserLoginDTO loginDTO) {
		// 一、验证码判断
		captchaHandler.setParams(loginDTO);
		captchaHandler.handle();

		// 二、登录流程
		// 得到用户名
		String username = loginDTO.getUsername();
		log.debug("用户{}正在登录...", username);

		// 查询用户信息
		User user = feignService.selectByUsername(username);

		if (user != null) {
			String userId = user.getId();

			if (user.getEnable()) {
				// 1、shiro登录认证
				loginHandler.setNext(userUpdateHandler);
				loginHandler.setParams(loginDTO);

				// 2、修改管理员上一次登录时间
				userUpdateHandler.setNext(emailSendHandler);
				userUpdateHandler.setParams(userId);

				// 3、邮件通知
				emailSendHandler.setNext(loginLogHandler);
				emailSendHandler.setParams(username);

				// 4、如果开启了系统日志,添加管理员登录日志
				loginLogHandler.setNext(redisCacheHandler);
				loginLogHandler.setParams(userId);

				// 5、从redis中删除并查询用户的权限保存到redis
				redisCacheHandler.setParams(username); // 这是最后一个处理流程

				// 开始执行处理链
				loginHandler.handle();
			} else {
				// 用户被锁定,抛出异常
				throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定,禁止登录!");
			}
		} else {
			throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在~");
		}
	}

	@Override
	public void updatePass(UserUpdateDTO userUpdateDTO) {
		feignService.updatePass(userUpdateDTO);
	}

}

这样设计的好处有:

  • 提高了代码的扩展性,当需要完善登录流程时,只需要再创建一个UserLoginHandler实现类;
  • 降低了类之间的耦合,原来所有的业务都在UserServiceImpl里做了,现在重构到了多个UserLoginHandler实现类里,当登录流程变更时,不需要在现有的代码上做很大的改动。 

你可能感兴趣的:(设计模式,python,开发语言)