在业务开发中,登录接口是非常常见的场景,随着业务的发展,需要支持多种登录形式
那么登陆接口该如何书写?
如何设计接口?
面对大量if else如何进行优化?
我们今天讲解的是在项目中如何应用策略模式。
登陆场景
登录方法
实现
策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
建立Springboot demo
https://start.spring.io/
1.SQL
代码语言:SQL复制CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`password` decimal(15,4) NOT NULL COMMENT '密码',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(30) NOT NULL DEFAULT 'sys' COMMENT '创建者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(30) NOT NULL DEFAULT 'sys' COMMENT '更新者',
PRIMARY KEY (`id`)
) COMMENT='用户信息表';
2.pom
代码语言:java复制<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>testdemo</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--akka-->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-slf4j_2.11</artifactId>
<version>2.5.16</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
<!--Mysql依赖包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid数据源驱动 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
新增一个controller
代码语言:java复制@Slf4j
@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping(value = "login/create")
public String loginVerify(@RequestBody LoginRequest request){
if(request == null) throw new RuntimeException("网络丢了,请稍后再试......");
return JSONObject.toJSONString(loginService.login(request));
}
@PostMapping(value = "add/user")
public String addUser(@RequestBody User user){
if(user == null) throw new RuntimeException("网络丢了,请稍后再试......");
return JSONObject.toJSONString(loginService.addUser(user));
}
}
新增一个service
代码语言:java复制public interface LoginService {
Boolean login(LoginRequest request);
Boolean addUser(User user);
}
新增一个impl
代码语言:java复制@Service
@Slf4j
public class LoginServiceImpl implements LoginService {
@Autowired
private UserMapper userMapper;
@Override
public Boolean login(LoginRequest request) {
var msg = "当前登录方式是";
if(request.getType() == 1){
log.info(msg "账号密码登录,用户名是{}密码是{}",request.getLoginName(),request.getPassword());
}
if(request.getType() == 2){
log.info(msg "验证码登录,用户名是{}密码是{}",request.getLoginName(),request.getPassword());
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean addUser(User user) {
user.setCreateBy(user.getUserName());
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
user.setUpdateBy(user.getUserName());
userMapper.insert(user);
return true;
}
}
@Mapper
代码语言:java复制public interface UserMapper extends BaseMapper<User> {
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="springboot.strategy.dao.UserMapper">
<resultMap id="BaseResultMap" type="springboot.strategy.entity.User">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="create_by" property="createBy" jdbcType="VARCHAR"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="update_by" property="updateBy" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectUserInfo" resultType="springboot.strategy.entity.User">
SELECT * FROM user WHERE user_name is not null order by create_time desc
</select>
</mapper>
package springboot.strategy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* @author zhaokk
* @create 2023-03-25 19:09
*/
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
private Date createTime;
/**
* 创建人
*/
private String createBy;
/**
* 修改时间
*/
private Date updateTime;
/**
* 修改人
*/
private String updateBy;
}
3.登录场景
1.策略模式
定义一个策略类
代码语言:java复制public class LoginStrategy {
private DynamicLoginService strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public Boolean login(LoginRequest request) {
return strategy.login(request);
}
}
实际调用
代码语言:java复制LoginStrategy strategy = new LoginStrategy();
if(request.getType == 1){
strategy.setStrategy(new AccountNamePasswordLoginServiceImpl());
}
strategy.login(request)
API = Application Programming Interface 程序之间的接口
SPI(service provider interface)机制是JDK内置的一种服务发现机制,可以动态的发现服务,即服务提供商,它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。目前这种大部分都利用SPI的机制进行服务提供.
在 JDK SPI 中,通过 ServiceLoader 加载实现类时,底层会使用反射机制加载并实例化对应的类。具体地,ServiceLoader 会通过线程上下文类加载器加载指定的服务接口的实现类,并调用其无参构造方法进行实例化。这意味着,实现类必须提供一个公共的无参构造方法,以便在加载时被正确实例化。
在 Spring SPI 中,通过 ApplicationContext 加载 Bean 时,底层也使用了反射机制来实例化对应的类。具体地,Spring 会扫描项目中所有的 class 文件,查找带有特定注解(例如 @Component、@Service 等)的类,并将其实例化成 Bean。当需要获取某个 Bean 时,Spring 会根据 Bean 的名称或类型,在容器的内部缓存中查找并返回相应的实例。
mysql-connector-java-5.1.35.jar
java.util.ServiceLoader
JDK8 VS JDK11
https://download.java.net/openjdk/jdk8u40/ri/openjdk-8u40-src-b25-10_feb_2015.zip
https://www.apiref.com/java11-zh/java.base/java/util/ServiceLoader.html
2.JDK SPI
META-INF/services新建一个service的全限定类名的文件
代码语言:java复制 ServiceLoader<DynamicService> serviceLoader = ServiceLoader.load(DynamicService.class);
System.out.println("Java SPI");
serviceLoader.forEach(Robot::sayHello);
为什么替换JDK自带的SPI?
2.SpringFactoriesLoader类的主要作用是通过类路径下的META-INF/spring.factories文件获取工厂类接口的实现类,初始化并保存在缓存中,以供Springboot启动过程中各个阶段的调用。Spring的自动化配置功能从而加载配置中的bean,也与此息息相关。
反射,属于主动调用,所以会触发类初始化
代码语言:java复制org.springframework.core.io.support.SpringFactoriesLoader#loadSpringFactories方法
既然能够获取所有的实现类,那我们能指定码?自带的方法是不支持的,我们需要自己改造
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.*;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author zhaokk
* @create 2023-03-27 11:45
*/
@Component
public class SpringFactoriesLoaderExtend{
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
//private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
private SpringFactoriesLoaderExtend() {
}
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>If a custom instantiation strategy is required, use {@link #loadFactoryNames}
* to obtain all registered factory names.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
* @see #loadFactoryNames
*/
public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryClass, "'factoryClass' must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" factoryClass.getName() "] names: " factoryNames);
}
List<T> result = new ArrayList<>(factoryNames.size());
for (String factoryName : factoryNames) {
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
Collection<List<String>> values = loadSpringFactories(classLoader, factoryClassName).values();
List<String> names = new ArrayList<>();
for (List<String> value : values) {
for (String s : value) {
names.add(s);
}
}
Collection<List<String>> values1 = loadSpringFactories(classLoader, factoryClassName).values();
List<String> collect = values1.stream().filter(item -> item.size() > 1).flatMap(set -> set.stream()).collect(Collectors.toList());
return names;
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader,String className) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
if (!result.isEmpty()) {
break;
}
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
if(factoryName.equals(className)){
result.add(factoryClassName, factoryName.trim());
break;
}
}
}
}
//cache.put(classLoader, result);
return result;
}catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location ["
FACTORIES_RESOURCE_LOCATION "]", ex);
}
}
@SuppressWarnings("unchecked")
private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
try {
Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
if (!factoryClass.isAssignableFrom(instanceClass)) {
throw new IllegalArgumentException(
"Class [" instanceClassName "] is not assignable to [" factoryClass.getName() "]");
}
return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();
}
catch (Throwable ex) {
throw new IllegalArgumentException("Unable to instantiate factory class: " factoryClass.getName(), ex);
}
}
}
4.有没有一种方法,等我们实际使用的时候在进行加载,并且还支持ByName获取呢?
代码语言:java复制ApplicationContext
实例化所有剩余的(非惰性init)singleton
→org.springframework.boot.SpringApplication#refreshContext
→→org.springframework.context.support.AbstractApplicationContext#refresh
→→→org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization
initializing all remaining singleton beans
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
// Register a default embedded value resolver if no bean post-processor
// (such as a PropertyPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}
// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}
// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);
//冻结所有的beanDefinition
// Allow for caching all bean definition metadata, not expecting further changes.
beanFactory.freezeConfiguration();
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
}
SPI实际上是“接口 策略模式 配置文件”实现的动态加载机制。在系统设计中,模块之间通常基于接口编程,不直接显示指定实现类。一旦代码里指定了实现类,就无法在不修改代码的情况下替换为另一种实现。为了达到动态可插拔的效果,java提供了SPI以实现服务发现。
扩展点开发指南 | Apache Dubbo
SPI 扩展使用手册 | Apache Dubbo
源码下载:
https://github.com/apache/dubbo-samples.git
Dubbo 扩展的特性
Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来,它改进了 JDK 标准的 SPI 以下问题:
JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
用户能够基于 Dubbo 提供的扩展能力,很方便基于自身需求扩展其他协议、过滤器、路由等。下面介绍下 Dubbo 扩展能力的特性。
按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用扩展类实例化,减少资源浪费。
增加扩展类的 IOC 能力。Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能。
增加扩展类的 AOP 能力。Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能。
具备动态选择扩展实现的能力。Dubbo 扩展会基于参数,在运行时动态选择对应的扩展类,提高了 Dubbo 的扩展能力。
可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。
提供扩展点的 Adaptive 能力。该能力可以使的一些扩展类在 consumer 端生效,一些扩展类在 provider 端生效。
从 Dubbo 扩展的设计目标可以看出,Dubbo 实现的一些例如动态选择扩展实现、IOC、AOP 等特性,能够为用户提供非常灵活的扩展能力。
4.Applicationcontext
加载bean
finishBeanFactoryInitialization(beanFactory);
注入Applicationcontext
维护枚举类
代码语言:java复制@Getter
@AllArgsConstructor
public enum DynamicStrategyEnum {
账号密码(DynamicEnum.账号密码.getCode(), UnRouteTaskServiceImpl.class),
验证码(DynamicEnum.验证码.getCode(), CancelRouteTaskServiceImpl.class),
人脸(DynamicEnum.人脸.getCode(), ChangeOrderTaskServiceImpl.class),
指纹(DynamicEnum.指纹.getCode(), RoutedAndUnDistributeServiceImpl.class),
微信登录(DynamicEnum.微信登录.getCode(), DistributeApplyOrderServiceImpl.class),
支付宝(DynamicEnum.支付宝.getCode(),UnApprovedSettlementServiceIimpl.class);
private Integer code;
private Class<? extends WmsDailySettlementService> clazz;
public static Class<? extends WmsDailySettlementService> getClazz(Integer param) {
if (StringUtils.isEmpty(param)) {
throw new NullPointerException("param is null");
} else {
for (DynamicStrategyEnum strategyEnum : DynamicStrategyEnum.values()) {
if (strategyEnum.getCode().equals(param)) {
return strategyEnum.getClazz();
}
}
throw new NullPointerException(String.format("param is null is %s", param));
}
}
}
5.注解驱动
注解是如何获取的?
https://blog.csdn.net/A_art_xiang/article/details/121954905
代码语言:Java复制findCandidateComponents
scanCandidateComponents ->
getMetadataReader ->
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
CachingMetadataReaderFactory::getMetadataReader -> new SimpleMetadataReader
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
if (definition instanceof AnnotatedBeanDefinition) {
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
if (StringUtils.hasText(beanName)) {
// Explicit bean name found.
return beanName;
}
}
// Fallback: generate a unique default bean name.
return buildDefaultBeanName(definition, registry);
}
新建一个注解驱动
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicHandlerType {
代码语言:Java复制DynamicEnum handler();
}
在实现类添加注解
@DynamicHandlerType(handler = DynamicEnum.账号密码登录)
在应用的地方
代码语言:Java复制private Map<DynamicEnum, DynamicLoginService> dynamicLoginServiceMap;
@Autowired
public void setDynamicLoginServiceMap(List<DynamicLoginService> list) {
dynamicLoginServiceMap = list.stream()
.collect(Collectors.toMap(
handler-> AnnotationUtils.findAnnotation(handler.getClass(), DynamicHandlerType.class).handler(),
handler-> handler));
}
}
DynamicLoginService dynamicLoginService = dynamicLoginServiceMap.get(DynamicEnum.tEnum(request.getType().toString()));
dynamicLoginService.dynamicLogin(request);
6.CompletableFuture
private static ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
static {
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(2);
taskExecutor.setKeepAliveSeconds(0);
taskExecutor.setQueueCapacity(0);
taskExecutor.setThreadNamePrefix("threadpool");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
}
CompletableFuture<Void> t1 = CompletableFuture.runAsync(() -> {
Service.login
}, taskExecutor);