最近业务代码编写中使用到了一个函数式接口 Consumer,巧妙地解决了代码复用的问题,既解决了业务需求,代码风格又优雅,而且高度内聚。下面直接上代码案例,然后再深入介绍Java8中的几个函数式接口:Function<T, R>ConsumerPredicateSupplier。最后结合使用场景以及Java逆向移植工具Retrolambda(点这了解Retrolambda)帮助读者加深对函数式接口的理解。
Consumer案例
需求背景
因涉及系统敏感信息,案例是经过脱敏、简化后的,不影响实际理解与使用,示例代码也是根据简化后的需求从头开始编写的。
有一个订单列表的需求,不同的用户查看到的订单列表数据是不一样的,规则如下:
- 超级管理员能查看所有订单,超级管理员能够根据不同的条件进行筛选,比如查看全部A类,比如查看单个企业,比如查看单个团队;
- A类管理员只能查看所有A类企业的订单,也能根据单个企业、或者单个团队的条件进行筛选;B类管理员只能查看所有B类企业的订单,其他和A类管理员一样
- 企业管理员能查看该企业下的所有订单,企业下面有很多团队,企业管理员也能根据团队进行筛选,
- 团队管理员只能查看本团队的所有订单
所以需要根据权限将订单列表进行过滤掉,也就是说需要根据当前用户角色,设置不同的WHERE条件,传到数据库里面去查询对应的数据。
编码实现
下面列出关键代码,主要关注点在Consumer的使用,像设计、编码是否合理可以忽略。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/admin")
public List<AdminOrderListVO> getAdminOrderList(AdminOrderListCommand command) {
List<Order> orders = orderService.getAdminOrderListByParam(command.to());
return orders.stream().map(AdminOrderListVO::from).collect(Collectors.toList());
}
@GetMapping("/type-admin")
public List<TypeAdminOrderListVO> getTypeAdminOrderList(TypeAdminOrderListCommand command) {
List<Order> orders = orderService.getTypeAdminOrderListByParam(command.to());
return orders.stream().map(TypeAdminOrderListVO::from).collect(Collectors.toList());
}
@GetMapping("/enterprise-admin")
public List<EnterpriseAdminOrderListVO> getEnterpriseAdminOrderList(EnterpriseAdminOrderListCommand command) {
List<Order> orders = orderService.getEnterpriseOrderListByParam(command.to());
return orders.stream().map(EnterpriseAdminOrderListVO::from).collect(Collectors.toList());
}
}
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public List<Order> getAdminOrderListByParam(AdminOrderListParam adminParam) {
OrderService.fillCommonCondition(adminParam::setEnterpriseType, adminParam::setEnterpriseId, adminParam::setTeamId);
return orderMapper.findAdminOrderListByParam(adminParam);
}
public List<Order> getTypeAdminOrderListByParam(TypeAdminOrderListParam typeAdminParam) {
OrderService.fillCommonCondition(typeAdminParam::setEnterpriseType,
typeAdminParam::setEnterpriseId, typeAdminParam::setTeamId);
return orderMapper.findTypeAdminOrderListByParam(typeAdminParam);
}
public List<Order> getEnterpriseOrderListByParam(EnterpriseAdminOrderListParam enterpriseAdminParam) {
OrderService.fillCommonCondition(enterpriseAdminParam::setEnterpriseType,
enterpriseAdminParam::setEnterpriseId, enterpriseAdminParam::setTeamId);
return orderMapper.findEnterpriseAdminOrderListByParam(enterpriseAdminParam);
}
public static void fillCommonCondition(Consumer<String> setEnterpriseType,
Consumer<Integer> setEnterpriseId, Consumer<Long> setTeamId) {
if (setEnterpriseType != null) {
setEnterpriseType.accept(CurrentUserUtil.currentEnterpriseType());
}
if (setEnterpriseId != null) {
setEnterpriseId.accept(CurrentUserUtil.currentEnterpriseId());
}
if (setTeamId != null) {
setTeamId.accept(CurrentUserUtil.currentTeamId());
}
}
}
分析
上面列出了Controller和Service,Controller有三个订单列表的接口,他们有不同的参数对象,接口逻辑都是先将参数对象转成Service的入参对象,调用Service的逻辑,最后将Service返回数据转成对应VO。重点是在Service里面,三个Service方法都共同调用了fillCommonCondition方法,这个方法的功能就是:动态地向不同对象中设置属性值,实现原理就是根据传进来的Consumer函数式接口,执行下传进来的方法,并且是带一个参数的,相当于动态调用了不同对象的Set方法,把当前用户某些属性设置到对象中。
不同的Consumer参数类型是可以不一样的,但是同一个字段,在不同对象中类型需要一样。其实fillCommonCondition方法不仅适用在订单列表,其实整个系统的权限控制都是这个逻辑,这种写法适用于所有需要权限控制的场景,不限对象类型,实现了代码高度复用,不然需要在每个接口手动调用当前参数对象的SET方法来设置值。
在上面例子中,我理解的就是将set方法作为参数传到另一个方法里面,然后去执行传进来的set方法,其他的函数式接口也是类似,只是根据方法参数和返回值分了类。以前实现动态方法调用基本就是使用反射,用起来比较繁琐,而且代码很僵硬。使用了函数式接口代码十分简洁,由此想深入理解下Java8中的几个函数式接口。
Function<T, R>
Function<T, R>首先是一个接口,里面有一个抽象方法,三个默认实现的方法,主要是R apply(T t)方法,实现Function接口就需要实现apply方法,比如x -> 2 * x就是一个函数式接口,可以转换成JDK1.7内部类,重写了apply方法的形式,代码如下
Function<Integer, Integer> lambda = x -> 2 * x;
Function<Integer, Integer> function = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer x) {
return 2 * x;
}
};
jdk源码里面的一个方法
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
这是java.util.stream.Stream的map方法,参数就是一个Function接口。在上面Consumer的案例中,最后一步转成VO的时候,使用了Stream中的map方法,传进去了from的静态方法。效果就是将List转化成List,对每一个Order,都会调用传进去的from方法。
下面通过两个Function<T, R>的例子来演示不同的调用方式,第一个案例是实际传的Function是有参数的,第二个时没有参数的。
案例一
public class FunctionTest {
public static void main(String[] args) {
FunctionTest functionTest = new FunctionTest();
String s = functionTest.doFunction(functionTest::hasOneParam, "s");
Integer integer = functionTest.doFunction(functionTest::increase, 6);
System.out.println(s);
System.out.println(integer);
}
public <T, R> R doFunction(Function<T, R> function, T param) {
return function.apply(param);
}
public <T> String hasOneParam(T param) {
return param.toString();
}
public Integer increase(Integer i) {
return i + 1;
}
}
//运行结果
s
7
Process finished with exit code 0
doFunction就是执行传进来的方法,而且该方法的参数也是传进来的,相当于动态调用了一遍方法。我们通过Retrolambda工具将上面的代码编译成JDK6的Class文件,然后用IDEA反编译打开看下里面的内容。
上面FunctionTest类编译以后的是三个文件,因为有两个Lambda表达式,在JDK6中是使用内部类来实现的,而内部类编译后是单独Class文件。
打开看文件内容
每个Lambda表达式对应一个类,这个类实现了Function接口,FunctionTestKaTeX parse error: $ within math modeLambda$2这个类的实例,这个实例是通过调用工厂方法得到的,doFunction中就是调用具体的实现类的apply方法,参数也传到具体方法里面去,这样就实现了动态方法调用。
FunctionTestKaTeX parse error: $ within math modeLambda$2多了一个属性,这个属性是被调用方法所属的类,通过工厂方法传进来,因为实例方法的调用必须指明是哪个实例,静态方法可以直接通过类名来调用。
案例二
public class FunctionTest2 {
public static void main(String[] args) {
FunctionTest2 functionTest2 = new FunctionTest2();
String s = functionTest2.doFunction(FunctionTest2::hasNoParam, functionTest2);
System.out.println(s);
}
public <T, R> R doFunction(Function<T, R> function, T param) {
return function.apply(param);
}
public String hasNoParam() {
return "A";
}
}
//运行结果
A
Process finished with exit code 0
编译后的内容
这个案例和案例一的区别就是被动态调用的方法是没有参数的,apply方法是必须要传一个参数,所以这里的参数变成了被动态调用方法所属的实例。从代码上看,区别就是FunctionTest2KaTeX parse error: $ within math modeLambda$1中apply方法的参数是原封不动地传到hasOneParam的形参里面去。
其他函数式接口
案例二的写法有点类似Supplier的功能,没有参数但是提供一个返回值。如果使用参数,不使用Function的返回值,就变成了Consumer,所以其他的一些函数式接口原理都是类似的,有些变换了形式,有些通过继承、添加默认实现方法扩展了功能,像下面这些:
- BiFunction<T, U, R> 传两个参数,带一个返回值
- Predicate 传一个参数,返回一个布尔类型的值
- Supplier 没有参数,直接获取返回值
- BiPredicate<T, U> 传两个参数,返回一个布尔类型的值
自定义函数式接口
JDK的java.util.function包提供了很多函数式接口,如果不满足业务需求,可以自定义函数式接口,比如下面是一个函数式接口,接收三个参数,带一个返回值
@FunctionalInterface
public interface MyFunction<T, V, R, P> {
R apply(T t, V v, P p);
}
也可以将一些参数设置成固定的类型,如String,Integer或者具体对象类型,如 R apply(T t, String v, List lists)。
函数式接口的使用还算简单的,就是把方法当做参数传到方法里面,只是以前我们是传值类型的参数。函数式接口里面还可以有逻辑,甚至可以函数式接口嵌套或者叠加使用,可以根据自己想象力和业务需求玩出更骚、更花的一些操作,总的来说函数式接口确实方便了编码,可以先学起来,多实践,慢慢理解。