业务场景
大家都知道,在浏览商城类应用时,涉及到个人信息或者将商品加入到购物车或者支付时是需要用户登录才可以操作的。试想,如果由你来完成用户登录的功能,你会怎么实现?是通过用户名、密码完成登录,还是短信验证码登录,还是通过微信登录,或者通过QQ登录?有如此多的实现方式,我想,很多人第一想法应该是通过用户名密码的方式来完成登录功能吧。但是如果应用访问量越来越多,流量越来越大,产生一个新的需求,需要满足微信、QQ登录,你会怎么充分利用现有代码进行扩展,并保证将来面对新需求的扩展性?这就是适配器模式和桥接模式要解决的问题,即提高程序的复用性和扩展性。
代码实现
用户名密码实现登录
首先,我们通过用户名密码的方式来完成登录功能,比较简单,先完成Mapper层,在完成service层,然后完成controller层,代码如下:
Mapper层
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
其中,UserInfo为用户表对应的实体类,本次实战中,通过Mybatis-Plus完成数据访问,使用Mybatis-Plus中自带的基础方法即可,因此Mapper中不需要单独写代码
Service层
UserService接口:
public interface UserInfoService extends IService<UserInfo> {
String login(String userName);
UserInfo register(String userName, String password);
}
UserServiceIpl代码:
@Service("userService")
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService {
@Resource
public UserInfoMapper userInfoMapper;
@Override
public String login(String userName) {
UserInfo user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
if (user != null) {
return user.getUserName() + "你好!";
}
//需要登录
return "需要先注册哦!";
}
@Override
public UserInfo register(String userName, String password) {
UserInfo user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
if (user != null) {
throw new UnsupportedOperationException("已经注册过了");
}
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setPassword(password);
userInfoMapper.insert(userInfo);
user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
return user;
}
}
代码中,如果用户没有注册过,那么首先需要让用户去注册,注册后登录直接返回登陆成功提示信息。本次代码实战中,注重设计模式的讲解,其他细节部分不做要求。
Controller层:
@RestController
public class LoginController {
@Autowired
@Qualifier(value = "userService")
private UserInfoService userInfoService;
@PostMapping("/welcome")
public String login(@RequestParam("userName") String userName){
return userInfoService.login(userName);
}
@PostMapping("/register")
public UserInfo register(@RequestParam("userName") String userName,
@RequestParam("password") String password){
return userInfoService.register(userName, password);
}
}
通过上述代码可以完成用户名密码登录功能,如果现在需要新增通过微信,qq或Gitee登录,该如何在不修改现有代码的基础上去完成功能的扩展呢?首先不能修改现有代码,所以不能直接在uservice中新增方法,此时,可以通过适配器模式。
适配器模式完成用户登录
适配器模式:适配器模式旨在将一个类的接口适配成用户所期待的接口,能够帮助不兼容的接口变得兼容,对于适配器模式的介绍,可以点进上面链接详细了解,这里主要是展示在实战的用法。
了解了适配器模式,来看看适配器模式的UML类图
各个角色作用如下:
Adaptee:原角色,即被适配角色,在该角色的基础上进行功能的扩展
Adapter:适配角色,适配器模式的核心角色,扩展的核心逻辑在该角色中体现(通常采用类适配模式)
Target:目标角色,对于需要扩展的核心方法,需要依据依赖倒置原则将其放在一个接口中,Adapter通过继承源角色并实现该接口的方式实现业务逻辑,所以该角色中封装了核心逻辑方法
Client:客户端,通过Target来完成调用
通过UML类图不难发现,Adapter是适配器模式的核心角色,这里思考一个问题:
为什么需要继承源角色?为什么适配角色要实现目标角色接口?
答案很简单:继承源角色为了实现代码复用,进行扩展的时候,需要使用原有代码,通过继承可以实现代码复用,而实现目标角色接口是为了实现功能扩展,扩展的功能都放置在目标角色接口中
下面开始实现适配器(以Gitee登录为例):
UML类图中,Adaptee角色就是我们UserServiceIpl类
首先,编写Target代码:
/**
* 这是适配器模式的目标角色,要扩展的方法写在这里
*/
public interface LoginTarget {
String LoginByGitee(String code, String state);
}
然后编写Adapter角色:
@Service( value = "loginAdapter")
public class LoginAdapter extends UserInfoServiceImpl implements LoginTarget {
@Value("${gitee.state}")
private String giteeState;
@Value("${gitee.token.url}")
private String giteeTokenUrl;
@Value("${gitee.user.url}")
private String giteeUserUrl;
@Value("${gitee.user.prefix}")
private String giteeUserPrefix;
@Override
public String LoginByGitee(String code, String state) {
//将gitee平台返回的state跟前后端商定好的state比较,如果不相同则抛出异常
if(!giteeState.equals(state)){
throw new UnsupportedOperationException("Invalid Operation!");
}
//携带code,向gitee平台申请token
String tokenUrl = giteeTokenUrl.concat(code);
JSONObject tokenResponse = HttpClientUtils.execute(tokenUrl, HttpMethod.POST);
String token = String.valueOf(tokenResponse.get("access_token"));
// 携带token请求用户信息
String userUrl = giteeUserUrl.concat(token);
JSONObject userInfo = HttpClientUtils.execute(userUrl, HttpMethod.GET);
//获取用户信息,存入数据库表中的数据用户名需要加上前缀GITEE@即state@,这是为了防止不同第三方平台登录时出现用户名相同的情况
String userName = giteeUserPrefix.concat(String.valueOf(userInfo.get("name")));
//密码跟用户名一致
String password = userName;
//自动注册登录
return autoRegisterAndLogin(userName, password);
}
private String autoRegisterAndLogin(String userName, String password){
if(checkUserExists(userName)){
return login(userName);
}
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setPassword(password);
//先注册,再登录
register(userName, password);
return login(userName);
}
}
HttpClient工具代码如下:
public class HttpClientUtils {
public static JSONObject execute(String url, HttpMethod httpMethod){
HttpRequestBase http = null;
try {
HttpClient client = HttpClients.createDefault();
//根据HttpMethod来创建HttpRequest
if(httpMethod == HttpMethod.GET){
http = new HttpGet(url);
}else{
http = new HttpPost(url);
}
HttpEntity entity = client.execute(http).getEntity();
return JSONObject.parseObject(EntityUtils.toString(entity));
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
http.releaseConnection();
}
}
}
通过Gitee完成第三方登录的过程很简单,大家可以直接搜索 :Gitee授权第三方登录或直接进入连接Gitee OAuth 文档,便可查看详细步骤,这里总结一下登录步骤:
1. 在浏览器输入网址:https://gitee.com/oauth/authorize?client_id="你的client_id"&redirect_uri="你的回调地址"&response_type=code&state=GITEE(实际上,这是随机生成的,将此参数发给Gitee平台后,gitee会原样返回给我们)
上述信息是你在gitee平台上完成授权后显示的信息,图示为我的信息
将信息填入到上述网址中即可
2. 获取token:https://gitee.com/oauth/token?grant_type=authorization_code&client_id=${gitee.clientId}&redirect_uri=${gitee.callback}&client_secret=${gitee.clientSecret}&code=“输入第一个网址后返回到回调地址的code码”
其中,$符号包括的是上图中我的个人信息,将他们配置到yml文件中而已,yml配置文件如下:
Adapter角色中的配置信息就是图中的信息,发送上述请求后,就可以获取到Token,获取到Token后,就可以进行第三步获取到用户信息
3.获取用户信息:https://gitee.com/api/v5/user?access_token=“第二步获取到的token”,根据的得到的用户信息可以完成登录
第二步和第三步都是在我们的回调接口中自动发送的,不需要手动发送,只有第一步是需要手动发送的(实际上,这一步应当是前端的工作,想象一下,平时在应用上进行第三方登录时,是不是只需要点击第三方应用图标即可,实际上,点击图标就是在发送第一个请求)
需要注意的是,通过第三方登录必须要自己设置回调接口,在发送第一个请求后,Gitee平台会向我们的回到接口返回state和code,然后才能进行第二步、第三步。
现在再来看Adapter中的代码,你会发现,其中复用了刚开始写的用户名密码登录的代码,因为通过第三步的请求得到用户名后,登录注册是一样的逻辑,只是通过第三方登录,可以使用用户的第三方账号,方便登录。有了适配器模式,以后如果有新的需求,需要增加其他第三方登录,只需要在Target中添加方法,然后新建一个Adapter实现即可,扩展性非常强
再看Controller层,回调接口是定义在controller中的
@RestController
public class LoginController {
@Autowired
@Qualifier(value = "loginAdapter")
private LoginAdapter loginAdapter;
// 回调接口
@GetMapping("/gitee")
public String gitee(String code, String state){
return loginAdapter.LoginByGitee(code, state);
}
}
在第一步发送请求后,gitee平台会给我们的回调地址返回State和Code所以回调接口会有两个参数。
前面已经说过,适配器模式是在不修改原有代码的前提下进行功能扩展。而桥接模式是对原有代码的重构,会产生风险,但是重构带来的扩展性和代码的整体性,可维护性其价值是很高的。
桥接模式实现用户登录
桥接模式:桥接模式旨在将抽象和实现解耦,即将抽象部分和实现部分分离开来,体现了“最少知识原则”。
UML类图:
Implementor:核心方法的承载接口(或者抽象类),封装了核心功能的所有方法
ConcreteImplementor:核心方法的具体子类,实现了承载接口中的方法,通常,相同的功能可以有不同的实现方式,比如登陆功能,可以通过微信,qq等,对于不同的实现方式只需要实现该接口进行实现即可,也便于维护
Abstraction:抽象角色,从UML类图中可以看出,抽象角色是直接暴露给客户端使用的,可知,客户端在使用时,只需要知道抽象类即可,对于Implementor根本没有必要知晓,即实现了抽象与实现解耦,该角色中也包含着核心功能方法,同Implementor中的方法一致,虽然代码略小冗余,但是同样为我们带来了低耦合性,高扩展性,符合“最少知识原则”
RefinedAbsraction:抽象角色子类,上面的抽象角色是不能直接给客户端使用的,需要创建他的具体子类,具体子类中需要实现方法的逻辑,依靠的是Implementor的具体子类来实现的,也就是说,只需要在抽象角色子类中调用Implementor的具体子类中的方法实现即可,这就需要在抽象角色中引入Implementor,搭建桥梁。
第一步:创建Implementor
/**
* 桥接模式的实现部分,此接口中定义了通用的方法,此处为login,register,valid
*/
public interface RegisterLoginInterface {
String login(String userName);
UserInfo register(String userName, String password);
Boolean checkUserExists(String userName);
String login3rd(HttpServletRequest httpServletRequest);
}
第二步:创建ConcreteImplementor
这里需要注意:由于Implementor中定义了五个方法,但是并不是每个方法都是子类需要去实现的,比如login方法、register方法、checkUserExists方法,这几个方法都是通用的,不同的登录方法只需要实现login3rd方法即可,因此,可以在ConcreteImplementor与Implementor中间加一层抽象类,将三个通用的方法给出具体的实现,示例中的抽象类对于接口中的方法以抛出异常的形式“实现”的,而给出了另外三个方法(common开头的三个方法)用来实现接口中的三个通用方法,是因为实现登录注册需要使用到持久层Mapper,但是在抽象类中是不能通过注解@Autowired注入持久层的,但是我们可以从其他能注入的地方通过传参的方式传入持久层(这里时UserInfoMapper),因此为了不破坏Implementor中方法的参数,才另写了三个common开头的方法来实现具体逻辑,给出代码入下:
/**
* 此类是一个抽象类,作用是将通用的方法放在此处并予以实现,供子类通用,需要在子类中单独实现的方法则不予以实现,留给子类去实现
* 可以实现代码复用,
*/
public abstract class AbstractRegisLoginFunction implements RegisterLoginInterface{
public String login(String userName){
throw new UnsupportedOperationException();
}
public UserInfo register(String userName, String password){
throw new UnsupportedOperationException();
}
public Boolean checkUserExists(String userName){
throw new UnsupportedOperationException();
}
public String commonLogin(String userName, UserInfoMapper userInfoMapper) {
UserInfo user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
if (user != null) {
return user.getUserName() + "你好!";
}
//需要登录
return "需要先注册哦!";
}
public UserInfo commonRegister(String userName, String password, UserInfoMapper userInfoMapper) {
UserInfo user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
if (user != null) {
throw new UnsupportedOperationException("已经注册过了");
}
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setPassword(password);
userInfoMapper.insert(userInfo);
user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
return user;
}
public Boolean commonCheckUserExists(String userName, UserInfoMapper userInfoMapper) {
UserInfo user = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
.eq(UserInfo::getUserName, userName)
.eq(UserInfo::getDeleted, Boolean.FALSE));
if (user != null) {
return true;
}
return false;
}
public String login3rd(HttpServletRequest httpServletRequest){
throw new UnsupportedOperationException();
}
}
然后编写Implementor的子类,子类需要继承上述抽象类并实现Implementor
默认登录方式:
@Component
public class RegisterLoginDefault extends AbstractRegisLoginFunction implements RegisterLoginInterface{
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public String login(String userName) {
return commonLogin(userName, userInfoMapper);
}
public UserInfo register(String userName, String password){
return commonRegister(userName, password, userInfoMapper);
}
public Boolean checkUserExists(String userName){
return commonCheckUserExists(userName, userInfoMapper);
}
/**
* 项目启动时,自动将该类实例加入到funcMap中
*/
@PostConstruct
private void initFuncMap(){
AbstractRegisterLoginComponentFactory.funcMap.put("Default", this);
}
}
第三方登录(Gitee为例):
/**
* 通过Gitee登录
*/
@Component
public class RegisterLoginByGitee extends AbstractRegisLoginFunction implements RegisterLoginInterface{
@Value("${gitee.state}")
private String giteeState;
@Value("${gitee.token.url}")
private String giteeTokenUrl;
@Value("${gitee.user.url}")
private String giteeUserUrl;
@Value("${gitee.user.prefix}")
private String giteeUserPrefix;
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public String login3rd(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String state = httpServletRequest.getParameter(("state"));
//将gitee平台返回的state跟前后端商定好的state比较,如果不相同则抛出异常
if(!giteeState.equals(state)){
throw new UnsupportedOperationException("Invalid Operation!");
}
//携带code,向gitee平台申请token
String tokenUrl = giteeTokenUrl.concat(code);
JSONObject tokenResponse = HttpClientUtils.execute(tokenUrl, HttpMethod.POST);
String token = String.valueOf(tokenResponse.get("access_token"));
// 携带token请求用户信息
String userUrl = giteeUserUrl.concat(token);
JSONObject userInfo = HttpClientUtils.execute(userUrl, HttpMethod.GET);
//获取用户信息,存入数据库表中的数据用户名需要加上前缀GITEE@即state@,这是为了防止不同第三方平台登录时出现用户名相同的情况
String userName = giteeUserPrefix.concat(String.valueOf(userInfo.get("name")));
//密码跟用户名一致
String password = userName;
//自动注册登录
return autoRegisterAndLogin(userName, password);
}
private String autoRegisterAndLogin(String userName, String password){
if(commonCheckUserExists(userName, userInfoMapper)){
return commonLogin(userName, userInfoMapper);
}
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setPassword(password);
//先注册,再登录
commonRegister(userName, password, userInfoMapper);
return commonLogin(userName, userInfoMapper);
}
/**
* 项目启动时,自动将该类实例加入到funcMap中
*/
@PostConstruct
private void initFuncMap(){
AbstractRegisterLoginComponentFactory.funcMap.put("GITEE", this);
}
}
注意这里的@PostConstruct注解,这个注解可以在Springboot项目启动后,首先扫面@Component注解的类并放入到容器中,然后立即调用@POSTConstruct注解的方法,这里是调用initFuncMap完成funcmap的初始化,AbstractRegisterLoginComponentFactory的代码后面会展示
第三步:创建Abstraction抽象角色
public abstract class AbstractRegisterLoginComponent {
// 引入RegisterLoginInterface搭建桥梁
protected RegisterLoginInterface registerLoginInterface;
public AbstractRegisterLoginComponent(RegisterLoginInterface registerLoginInterface){
validate(registerLoginInterface);
this.registerLoginInterface = registerLoginInterface;
}
protected final void validate(RegisterLoginInterface registerLoginInterface){
if(! (registerLoginInterface instanceof RegisterLoginInterface)){
throw new UnsupportedOperationException();
}
}
public abstract String login(String userName);
public abstract UserInfo register(String userName, String password);
protected abstract Boolean checkUserExists(String userName);
public abstract String login3rd(HttpServletRequest httpServletRequest);
}
抽象角色中的方法跟Implementor中的方法一致,你可能会角色代码冗余,但这是必须的,为了实现抽象与实现的分离、解耦,这既是代价。代码中,抽象角色需要关联Implementor角色(本例中是registerLoginInterface),起到了桥梁的作用,抽象角色正是通过Implementor来实现功能的。这里关联的是顶级接口RegisterLoginInterface,那么需要不同的登录方式时,只需要传入相应的implementor的子类即可,这就是里氏替换原则的体现
第四部:创建抽象角色的子类
public class RegisterLoginComponent extends AbstractRegisterLoginComponent{
public RegisterLoginComponent(RegisterLoginInterface registerLoginInterface) {
super(registerLoginInterface);
}
@Override
public String login(String userName) {
return registerLoginInterface.login(userName);
}
@Override
public UserInfo register(String userName, String password) {
return registerLoginInterface.register(userName, password);
}
@Override
protected Boolean checkUserExists(String userName) {
return registerLoginInterface.checkUserExists(userName);
}
@Override
public String login3rd(HttpServletRequest httpServletRequest) {
return registerLoginInterface.login3rd(httpServletRequest);
}
}
第五步:Client客户端调用:
Service 层:
@Service
public class UserBridgeServiceImpl implements UserBridgeService {
@Override
public String login(String userName) {
AbstractRegisterLoginComponent abstractRegisterLoginComponent = AbstractRegisterLoginComponentFactory.getComponent("Default");
return abstractRegisterLoginComponent.login(userName);
}
@Override
public UserInfo register(String userName, String password) {
AbstractRegisterLoginComponent abstractRegisterLoginComponent = AbstractRegisterLoginComponentFactory.getComponent("Default");
return abstractRegisterLoginComponent.register(userName, password);
}
@Override
public String login3rd(HttpServletRequest httpServletRequest, String type) {
AbstractRegisterLoginComponent abstractRegisterLoginComponent = AbstractRegisterLoginComponentFactory.getComponent(type);
return abstractRegisterLoginComponent.login3rd(httpServletRequest);
}
}
service中的AbstractRegisterLoginComponentFactory是对抽象类子类的缓存,提高效率,代码如下:
public class AbstractRegisterLoginComponentFactory {
//缓存component对象
public static Map<String, AbstractRegisterLoginComponent> componentMap = new ConcurrentHashMap<>();
// 缓存RegisterLoginInterface 对象
public static Map<String, RegisterLoginInterface> funcMap = new ConcurrentHashMap<>();
public static AbstractRegisterLoginComponent getComponent(String type){
AbstractRegisterLoginComponent abstractRegisterLoginComponent = componentMap.get(type);
if(abstractRegisterLoginComponent == null){
// 双重锁
synchronized (componentMap){
abstractRegisterLoginComponent = componentMap.get(type);
// 不存在则新建
if(abstractRegisterLoginComponent == null){
abstractRegisterLoginComponent = new RegisterLoginComponent(funcMap.get(type));
componentMap.put(type, abstractRegisterLoginComponent);
}
}
}
return abstractRegisterLoginComponent;
}
}
Controller:
@RestController
@RequestMapping("/bridge")
public class UserBridgeController {
@Autowired
private UserBridgeService userBridgeService;
@GetMapping("/third")
public String login3rd(HttpServletRequest request){
return userBridgeService.login3rd(request, "GITEE");
}
}
至此,完成了适配器模式和桥接模式对登录功能的实现,总的来说:
适配器模式可以在不修改原有代码的情况下实现对功能的扩展,适配其他接口;
桥接模式,需要重构原有代码,但是可以给之后的程序留下很大的扩展新和可维护性,对于桥接模式的登录,如果还希望增加其他第三方登录方式,只需要增加一个Implementor角色的自类即可,并且只需要增加子类即可,对于抽象角色及其子类角色不需要修改,扩展性非常强。
适配器模式和桥接模式的实战就到此为止,记住UML类图对今后的开发有很大帮助,可以根据实际情况来进行开发,有效使用设计模式能够给自己的程序带来很高的扩展性和可维护性。