I have created a strategy machine and I would like to add it to the Spring toolset, is that okay?
For example, we have an exception handling class with the following code:
if(e instanceof AException) {}
else if (e installof BException){}
else if ....
else ...
In this example, we will write a large number of if else. I think this is not elegant. If using a strategy machine, it can be much more elegant. Example:
@Slf4j
public class ExceptionHandler {
private static final StrategyMachine<String, Context, Result<Object>> strategyMachine;
private static final Action<Context, Result<Object>> HttpMediaTypeNotAcceptableExceptionAction = ExceptionHandler::getResult;
private static final Action<Context, Result<Object>> HttpRequestMethodNotSupportedExceptionAction = ExceptionHandler::getResult;
private static final Action<Context, Result<Object>> ResponseStatusExceptionAction = ExceptionHandler::getResult;
private static final Action<Context, Result<Object>> BaseExceptionAction = context -> {
BaseException e = (BaseException) context.getThrowable();
return Result.fail(e.getStatus().code(),e.getStatus().message(),null,e.getTips()==null?e.getStatus().message():e.getTips());
};
private static final Action<Context, Result<Object>> ExecutionExceptionAction = context -> {
if (context.getThrowable().getCause() instanceof BaseException baseException){
return BaseExceptionAction.apply(context);
}
return Result.fail(Status.Fail.code(),context.getThrowable().getMessage(),null,Status.Fail.message());
};
private static final Action<Context, Result<Object>> MethodArgumentNotValidExceptionAction = context -> {
MethodArgumentNotValidException e = (MethodArgumentNotValidException) context.getThrowable();
FieldError fieldError = e.getBindingResult().getFieldError();
return paramsErrorResult(fieldError,context);
};
private static final Action<Context, Result<Object>> BindExceptionAction = context -> {
BindException e = (BindException) context.getThrowable();
FieldError fieldError = e.getBindingResult().getFieldError();
return paramsErrorResult(fieldError,context);
};
private static final Action<Context, Result<Object>> TokenExpiredExceptionAction = context -> Result.fail(Status.TokenExpired.code(), context.getThrowable().getMessage(),null, Status.TokenExpired.message());
private static final Action<Context, Result<Object>> JWTVerificationExceptionAction = context -> Result.fail(Status.TokenIllegal.code(), context.getThrowable().getMessage(),null, Status.TokenIllegal.message());
private static final Action<Context, Result<Object>> ServletExceptionAction = context -> {
if (context.getThrowable().getCause()!=null) {
return apply(context.getThrowable().getCause());
}
return Result.fail(Status.RequestParamError.code(), context.getThrowable().getMessage(),null, Status.RequestParamError.message());
};
private static final Action<Context, Result<Object>> ParentExceptionAction = context -> apply(context.getThrowable().getCause());
private static final Action<Context, Result<Object>> OtherExceptionAction = context -> {
if (context.getThrowable().getCause()!=null) {
return ParentExceptionAction.apply(context);
}
return Result.fail(Status.Fail.code(),context.getThrowable().getMessage(),null, Status.Fail.message());
};
static {
StrategyMachineBuilder<String,Context, Result<Object>> machineBuilder = StrategyMachineFactory.create();
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof HttpMediaTypeNotAcceptableException ).perform(HttpMediaTypeNotAcceptableExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof HttpRequestMethodNotSupportedException).perform(HttpRequestMethodNotSupportedExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof ResponseStatusException).perform(ResponseStatusExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof BaseException).perform(BaseExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof ExecutionException).perform(ExecutionExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof MethodArgumentNotValidException).perform(MethodArgumentNotValidExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof BindException).perform(BindExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof TokenExpiredException).perform(TokenExpiredExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof JWTVerificationException).perform(JWTVerificationExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable() instanceof ServletException).perform(ServletExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable().getCause()==null).perform(OtherExceptionAction);
machineBuilder.of("").when((s, context) -> context.getThrowable().getCause()!=null).perform(ParentExceptionAction);
strategyMachine = machineBuilder.build("exception");
}
public static Result<Object> apply(Throwable e){
return apply(null,null,e);
}
public static Result<Object> apply(HttpServletResponse response, Throwable e){
return apply(null, response, e);
}
public static Result<Object> apply(HttpServletRequest request,HttpServletResponse response, Throwable e){
if (e instanceof BaseException baseException) {
log.warn("code:{},message:{}", baseException.getStatus().code(),baseException.getStatus().message());
if (baseException.getCause()!=null) {
log.error("",baseException.getCause());
}
} else {
log.error("",e);
}
Result<Object> apply = strategyMachine.apply("", new Context(request,response,e));
if (response!=null) {
HttpStatus httpStatus = HttpStatus.OK;
if (apply.getCode()== Status.TokenExpired.code()
||apply.getCode()== Status.TokenIllegal.code()
||apply.getCode()== Status.UserDisabled.code()
||apply.getCode()== Status.AdminDisabled.code()){
httpStatus = HttpStatus.UNAUTHORIZED;
}
response.setStatus(httpStatus.value());
}
return apply;
}
private static Result<Object> getResult(Context context){
BaseStatus status1 = Status.Fail;
return Result.fail(status1.code(), context.getThrowable()==null?"":context.getThrowable().getMessage(),null,context.getThrowable()==null?status1.message():context.getThrowable() instanceof BaseException e&&e.getTips()!=null?e.getTips():status1.message());
}
private static Result<Object> paramsErrorResult(FieldError fieldError, Context context){
if (fieldError==null){
return Result.fail(Status.RequestParamError.code(), "",null, Status.RequestParamError.message());
}
Locale locale = Locale.CHINA;
if (context.getRequest()!=null&&context.getRequest().getLocale()!=null) {
locale = context.getRequest().getLocale();
}
return Result.fail(Status.RequestParamError.code(), Status.RequestParamError.message(),fieldError, messageSource().getMessage(Objects.requireNonNull(fieldError.getCode()),new Object[]{fieldError.getField()}, locale));
}
private static MessageSource messageSource(){
return SpringContextUtil.getApplicationContext().getBean("validationMessageSource",MessageSource.class);
}
@Getter
@AllArgsConstructor
public static class Context {
private HttpServletRequest request;
private HttpServletResponse response;
private Throwable throwable;
}
}
This eliminates all if else and can even split files into n subclasses, completely decoupling them
Firstly, my core idea is what the strategy machine should do. I believe that the core problem that the strategy machine needs to solve is to handle different things in different scenarios and under different conditions, and obtain different results. This is a typical strategy pattern, which is not as bulky as a state machine, but very lightweight
Therefore, after clarifying the purpose, the idea becomes very simple, nothing more than about what strategy, under what conditions, what code to execute, and what results to return This is a very typical DSL scenario
Therefore, I first constructed the syntax sugar: strategy.of().when().perform()
or strategy.of().perform()
, because sometimes we don't need conditions
The core code is as follows:
/**
* {@link Of}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
public interface Of<S,C,R> {
When<S,C,R> when(Condition<S,C,R> condition);
StrategyMachineBuilder<S,C,R> perform(Action<C,R> action);
}
/**
* {@link When}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
public interface When<S,C,R> {
StrategyMachineBuilder<S,C,R> perform(Action<C,R> action);
}
/**
* {@link Condition}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
public interface Condition<S,C,R> {
boolean isSatisfied(S s,C c);
}
/**
* {@link Action}
* @param <C> context
* @param <R> result
*/
public interface Action<C,R> {
R apply(C c);
}
/**
* {@link Strategy}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
public interface Strategy<S, C, R> {
S strategy();
Condition<S,C,R> condition();
Action<C, R> action();
Strategy<S,C,R> strategy(S s);
Strategy<S,C,R> condition(Condition<S,C,R> condition);
Strategy<S,C,R> action(Action<C,R> action);
}
/**
* {@link StrategyMachine}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
public interface StrategyMachine<S,C,R> {
R apply(S s, C c);
}
/**
* {@link StrategyMachineBuilder}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
public interface StrategyMachineBuilder<S,C,R> {
Of<S,C,R> of(S s);
StrategyMachine<S,C,R> build(String id);
}
So, the architecture has been established, and implementation is very simple
package com.dv.commons.base.strategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* {@link StrategyMachineBuilderImpl}
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
class StrategyMachineBuilderImpl<S,C,R> implements StrategyMachineBuilder<S,C,R>{
private final Map<S, List<Strategy<S,C,R>>> map = new ConcurrentHashMap<>();
@Override
public Of<S, C, R> of(S s) {
map.computeIfAbsent(s, k -> new ArrayList<>());
Strategy<S,C,R> strategy = new StrategyImpl();
map.get(s).add(strategy);
return new OfImpl(strategy);
}
@Override
public StrategyMachine<S, C, R> build(String id) {
StrategyMachineImpl<S, C, R> machine = new StrategyMachineImpl<>(map);
StrategyCache.put(id, machine);
return machine;
}
public class OfImpl implements Of<S,C,R>{
private final Strategy<S,C,R> strategy;
OfImpl(Strategy<S,C,R> strategy){
this.strategy = strategy;
}
@Override
public When<S, C, R> when(Condition<S,C,R> condition) {
this.strategy.condition(condition);
return new WhenImpl(strategy);
}
@Override
public StrategyMachineBuilder<S, C, R> perform(Action<C, R> action) {
this.strategy.action(action);
return StrategyMachineBuilderImpl.this;
}
}
public class WhenImpl implements When<S,C,R> {
private final Strategy<S,C,R> strategy;
WhenImpl(Strategy<S,C,R> strategy){
this.strategy = strategy;
}
@Override
public StrategyMachineBuilder<S, C, R> perform(Action<C, R> action) {
this.strategy.action(action);
return StrategyMachineBuilderImpl.this;
}
}
public class StrategyImpl implements Strategy<S, C, R> {
private S strategy;
private Condition<S,C,R> condition;
private Action<C, R> action;
@Override
public S strategy() {
return this.strategy;
}
@Override
public Condition<S,C,R> condition() {
return this.condition;
}
@Override
public Action<C, R> action() {
return this.action;
}
@Override
public Strategy<S, C, R> strategy(S s) {
this.strategy = s;
return this;
}
@Override
public Strategy<S, C, R> condition(Condition<S,C,R> condition) {
this.condition = condition;
return this;
}
@Override
public Strategy<S, C, R> action(Action<C, R> action) {
this.action = action;
return this;
}
}
}
package com.dv.commons.base.strategy;
import java.util.List;
import java.util.Map;
/**
* Strategy Machine Impl
* @param <S> strategy
* @param <C> context
* @param <R> result
*/
class StrategyMachineImpl<S,C,R> implements StrategyMachine<S,C,R> {
private final Map<S, List<Strategy<S,C,R>>> map;
public StrategyMachineImpl(Map<S, List<Strategy<S,C,R>>> map){
this.map = map;
}
@Override
public R apply(S s, C c) {
List<Strategy<S, C, R>> strategies = map.get(s);
if (strategies==null||strategies.isEmpty()){
throw new RuntimeException("no strategy found for "+s);
}
for (Strategy<S, C, R> strategy : strategies) {
// if condition is null
if (strategy.condition()==null) {
return strategy.action().apply(c);
}
// 如果有condition,先判断是否满足condition,满足则执行action
if (strategy.condition().isSatisfied(s,c)){
return strategy.action().apply(c);
}
}
// 未发现策略关于s的condition
throw new RuntimeException("no strategy found of met condition for "+s);
}
}
package com.dv.commons.base.strategy;
/**
* Strategy Machine Factory
*/
public class StrategyMachineFactory {
public static <S,C,R> StrategyMachineBuilder<S,C,R> create() {
return new StrategyMachineBuilderImpl<>();
}
public static <S,C,R> StrategyMachine<S,C,R> get(String id) {
return (StrategyMachine<S, C, R>) StrategyCache.get(id);
}
}
package com.dv.commons.base.strategy;
import java.util.Map;
/**
* {@link StrategyCache}
*/
class StrategyCache {
private static final Map<String,StrategyMachine<?,?,?>> CACHE = new java.util.concurrent.ConcurrentHashMap<>();
public static void put(String id, StrategyMachine<?,?,?> machine) {
CACHE.put(id, machine);
}
public static StrategyMachine<?,?,?> get(String id) {
return CACHE.get(id);
}
}
Example: Under the age of 12, take 20 milligrams of medication per day; 12-18 years old, taking 30 milligrams a day 18-30 years old, taking 40 milligrams a day 30-50 years old, taking 45 milligrams a day Eating 42 milligrams for those over 50 years old
class MedicineStrategy {
private static StrategyMachine<String, MedicineContext, Void> strategy;
static {
StrategyMachineBuilder<String, MedicineContext, Void> machineBuilder = StrategyMachineFactory.create();
strategy = machineBuilder
.of("").when((s, c) -> c.age < 12).perform((c) -> {
System.out.println("Under the age of 12, take 20 milligrams of medication per day;");
return Void.TYPE.cast(null);
})
.of("").when((s, c) -> c.age >= 12 && c.age < 18).perform((c) -> {
System.out.println("12-18 years old, taking 30 milligrams a day");
return Void.TYPE.cast(null);
})
.of("").when((s, c) -> c.age >= 18 && c.age < 30).perform((c) -> {
System.out.println("18-30 years old, taking 40 milligrams a day");
return Void.TYPE.cast(null);
})
.of("").when((s, c) -> c.age >= 30 && c.age < 50).perform((c) -> {
System.out.println("30-50 years old, taking 45 milligrams a day");
return Void.TYPE.cast(null);
})
.of("").when((s, c) -> c.age >= 50).perform((c) -> {
System.out.println("Eating 42 milligrams for those over 50 years old");
return Void.TYPE.cast(null);
})
.build("medicine");
}
public static StrategyMachine<String, MedicineContext, Void> get() {
// StrategyMachine<String, MedicineContext, Void> strategy = StrategyMachineFactory.get("medicine");
return strategy;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class MedicineContext {
private int age;
}
public static void main(String[] args) {
get().apply("", new MedicineContext(10));
}
}
Example2:
StrategyMachineBuilder<String, StrategyContext, Number> machineBuilder = StrategyMachineFactory.create();
machineBuilder.of("加法").perform(strategyContext -> strategyContext.a + strategyContext.b);
machineBuilder.of("减法").perform(strategyContext -> strategyContext.a - strategyContext.b);
machineBuilder.of("乘法").perform(strategyContext -> strategyContext.a * strategyContext.b);
// 除法,当c==1时,忽略小数位, 当c==2时不忽略
machineBuilder.of("除法").when((s, strategyContext) -> strategyContext.c == 1).perform(strategyContext -> strategyContext.a / strategyContext.b);
machineBuilder.of("除法").when((s, strategyContext) -> strategyContext.c == 2).perform(strategyContext -> (strategyContext.a * 1.0d) / (strategyContext.b * 1.0d));
StrategyMachine<String, StrategyContext, Number> strategyMachine = machineBuilder.build("test");
// StrategyMachine<String, StrategyContext, Number> strategyMachine = StrategyMachineFactory.get("test");
System.out.println(strategyMachine.apply("加法", new StrategyContext(1, 2, 1)));
System.out.println(strategyMachine.apply("减法", new StrategyContext(1, 2, 1)));
System.out.println(strategyMachine.apply("乘法", new StrategyContext(1, 2, 1)));
System.out.println(strategyMachine.apply("除法", new StrategyContext(1, 2, 1)));
System.out.println(strategyMachine.apply("除法", new StrategyContext(1, 2, 2)));
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class StrategyContext {
private int a;
private int b;
private int c;
}
Comment From: bclozel
Thanks for reaching out. This doesn't look specific to Spring at all and would be generally useful outside of Spring. Maybe consider contributing it as an independent library? I don't think we should integrate this in Spring Framework as I see no use case in our current codebase right now.
Thanks!