imango

imango 查看完整档案

成都编辑湖南大学  |  数学与计量经济 编辑宇创达  |  边java边撸铁 编辑填写个人主网站
编辑

爱吃芒果爱撸铁的程序猿一枚
2019-09-05 这次再换个霸气点头像,嘎嘎嘎~┗|`O′|┛
2019-03-08 上面是4年前的留言了。。。现在应该是爱吃芒果,爱撸铁的程序猿一枚,哈哈哈哈哈,我好歹也撸铁两年了多了,这次顺带换个正面形象的头像... o( ̄▽ ̄)d

个人动态

imango 回答了问题 · 1月17日

如何一个VO实体在多个接口中返回的字段不一样呢?

起初第一眼看到题主的问题,我是有点匪夷所思的。。。主要是下面这段话
image.png

写接口,固定字段返回,看起来都很正常,那解决方案不就是

  1. 列表接口返回对象UserListVO,里面有3个字段
  2. 详情接口返回对象UserDetailVO,里面有4个字段

这跟jackson配置null是否返回,毫无关系啊,jackson只是一个序列化工具,但是不同接口返回不同字段这是业务逻辑,就需要创建类才可以解决啊。

创建两个类真的就是很难了么。。。那为啥非要只用一个UserVO对象来表示两个不同接口的返回呢?假如以后新增还有其他和User相关的接口,是不是要把再在UserVO类中塞满一堆字段,然后不同接口返回不同个数的字段呢,这样索性不要UserVO得了,直接实体对象User返回算了嘛。

诚然,就算是按照myskies的方式做了分组,这是一种解决题主问题的方案,但是但是,如果是实际公司业务开发,我想没有哪个兄弟愿意接手在一个大而全的VO对象去找他们需要的分组叭。。。起码要是我,我是要哭的,字段少还好,字段多了。。。@JsonView中各种取值是要看花了的哈

不过吐槽归吐槽,上面只是我作为同为程序员角色,站在同一角度提出的小意见,但是如果作为思否中问题和回答问题的角色,也就是相当于甲方乙方的角度来说的话,我还是要来想办法解决题主的问题
image

首先还是先捋一下当前的思路,以及需要解决的问题

假设现在就是UserService中的两个方法返回

public interface UserService {
    // 希望返回id,name,area
    List<UserVO> list();
    // 希望返回id,name,area,pwd
    UserVO detail();
}

虽然是两个方法,但是都是返回的同一个关联类UserVO的对象,然而又需要不同字段的返回结果。同一个类,但是不同方法返回不同的字段。你细品。。。不对啊,这不可能啊,字段是跟Class相关的,而Class本身就是独一份,咋能说运行时变来变去呢,毕竟UserVO的对象可是需要Class来实例化的啊

所以如果按照上面那种写法是根本不可能的,所以必须是list()detail()要返回不同的类,但是之前已经感受到了甲方的需求了,就是不想多创建两个类,那咋办?

当然就是回到Map啦,万能Map,毕竟Map的数据结构跟POJO的对象很类似嘛,都是键值对。
因此UserService就变成了

public interface UserService {
    // 希望返回id,name,area
    List<Map> list();
    // 希望返回id,name,area,pwd
    Map detail();
}

啊~多么朴素的写法啊(谁在我小组这么写,我就要杀人了啊!!)

那再看看实现,都这么返回Map,那还不无法无天,想怎么写就怎么写咯,当然这里用到了CGLBBeanMapspring自己把CGLIB也打入到项目中了,所以可以直接用SpringBeanMap,用BeanMap直接把UserVO转换成Map

所以UserService的实现类UserServiceImpl可能是这样写的

@Service
public class UserServiceImpl implements UserService {

    @Override
    public List<Map> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        List<Map> map = userVOList.stream()
                                  .map(BeanMap::create)
                                  .peek(beanMap -> beanMap.remove("pwd"))
                                  .collect(Collectors.toList());
        return map;
    }
    
    @Override
    public Map detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        BeanMap beanMap = BeanMap.create(userVO);
        return beanMap;
    }
}

虽然这种方案确实能解决问题,但是估计甲方会把我头打爆,这让甲方改多少东西,多少逻辑,而且如果需要扩展的话,比如用户详情detail()中突然不返回id了,岂不是还要在detail()方法体中增加remove的操作,实在太不开闭原则了

因此回归甲方需求的本身,那就是希望在detail()list()方法主体不进行太大改动的基础上实现功能,好歹也是什么方法返回什么字段应该是可配置的吧,这样后续修改也很容易,不用改方法主体,改配置就可以了

所以,Spring AOP+注解,上。

题主UserService的实现类UserServiceImpl估计是这样写的

@Service
public class UserServiceImpl implements UserService {

    @Override
    public UserVO detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        return userVO;
    }

    @Override
    public List<UserVO> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        return userVOList;
    }
}

现在要控制不同方法返回的UserVO字段不一样,先不说咋实现,咱们先把咋配置整好,直接整一个注解配置上。所以注解FieldLimit应运而生,就一个属性,配置需要返回哪些字段的名称

// 注:选择实现方案不同,这里的Retention取值可以是RetentionPolicy.SOURCE
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FieldLimit {
    // 默认{}就是返回所有的字段
    String[] value() default {};
}

FieldLimit把我们的方法装饰一下

@Service
public class UserServiceImpl implements UserService {

    // 不填写任何字段就是返回所有的字段
    @Override
    @FieldLimit
    public UserVO detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        return userVO;
    }

    @Override
    @FieldLimit({"id", "name", "area"})
    public List<UserVO> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        return userVOList;
    }
}

那接下来就是怎么实现了,显然刚才提到了AOP,但是AOP只是一个桥梁,是一个把处理不同字段的逻辑移到切面的一个桥梁,真正到底怎么实现不同方法返回不同的字段,仅仅靠AOP是解决不了,就像最开始我提到的,这个本质就是不同的业务,其实是需要两个不同返回对象来处理的,但是但是呢,甲方懒了亿点点,所以需要我们来用一些好工具帮他实现

即:我们需要帮甲方生成两个类,两个类的字段是有限制的,限制来源于FieldLimit(当然由于现在题主提到的需求中,detail()方法是返回所有字段的,所以准确来说只是生成一个类)

咋运行时生成类呢,当然用咱们的字节码编程啦。借助偏友好点API的用Javassist嘛(主要前段时间才接触过,哈哈哈)

由于本质上不同方法返回的类肯定是不一样的,所以就不能写返回UserVO了,得改成一个通用的,可以用Object,但是那也太抽象了,所以干脆做一个标记接口,返回接口,然后我们生成的新类去实现这个接口就可以了

public interface IBaseVO {
}

有了这个接口,这时候UserService就变成了

public interface UserService {

    IBaseVO detail();
    List<IBaseVO> list();
}

由于接口发生了变化,当然实现类也要做修改,不过不用改动太大,只是改改返回的类型,然后记得给UserVO实现接口IBaseVO即可

@Service
public class UserServiceImpl implements UserService {

    // 不填写任何字段就是返回所有的字段
    @Override
    @FieldLimit
    public IBaseVO detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        return userVO;
    }

    @Override
    @FieldLimit({"id", "name", "area"})
    public List<IBaseVO> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        return userVOList;
    }
}

那接下来就是最关键的地方了,怎么用切面实现生成新类的效果,直接上代码,切面类FieldLimitAspect

@Aspect
@Component
public class FieldLimitAspect {

    @Autowired
    private List<ReturnTypeHandler> handlers;

    @Pointcut(value = "@annotation(fieldLimit)")
    public void pointcut(FieldLimit fieldLimit) {
    }

    @Around(value = "pointcut(fieldLimit)")
    public Object around(ProceedingJoinPoint joinPoint, FieldLimit fieldLimit) throws Throwable {
        // 获取该方法返回结果
        Object result = joinPoint.proceed();
        // 若没有限制的字段,则直接返回原结果
        String[] fieldLimitNameArray = fieldLimit.value();
        if (fieldLimitNameArray.length == 0) return result;

        // 获取执行方法的返回类型
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Type genericReturnType = method.getGenericReturnType();
        ResolvableType resolvableType = ResolvableType.forType(genericReturnType);
        Optional<ReturnTypeHandler> returnTypeHandlerOptional = handlers.stream()
                .filter(handler -> handler.isNeedProcess(resolvableType)).findFirst();
        // 没有处理的Handler,则直接返回
        if (!returnTypeHandlerOptional.isPresent()) return result;
        ReturnTypeHandler returnTypeHandler = returnTypeHandlerOptional.get();

        // 若方法返回为空,就不用处理了
        if (returnTypeHandler.isEmpty(result)) return result;

        String classPath = this.buildClassName(method);
        Class<?> resultClass = returnTypeHandler.getRawClass(result);
        // 这是需要返回的字段set
        Set<String> fieldLimitNameSet = Arrays.stream(fieldLimitNameArray).collect(Collectors.toSet());
        // 这是返回的原始实体类应该有的字段在经过了需要返回的set过滤
        List<Field> fieldLimitFields = Arrays.stream(resultClass.getDeclaredFields())
                .filter(field -> fieldLimitNameSet.contains(field.getName()))
                .collect(Collectors.toList());
        // 如果过滤后没有任何一个字段,则直接返回原结果
        if (fieldLimitFields.isEmpty()) return result;
        Class newClass = NewClassBuilder.getNewClass(classPath, fieldLimitFields);

        result = returnTypeHandler.newInstance(newClass, result);
        return result;
    }

    private String buildClassName(Method method) {
        String declaringClassName = method.getDeclaringClass().getSimpleName();
        String methodName = method.getName();
        String returnTypeClassName = method.getReturnType().getSimpleName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        String className = Stream.concat(Stream.of(declaringClassName, methodName, returnTypeClassName),
                Arrays.stream(parameterTypes).map(Class::getSimpleName)).collect(Collectors.joining("$"));
        return className;
    }
}

emmm,由于我不想写死很多东西,所以FieldLimitAspect并没有把所有的逻辑包含进去,仅仅是包含了如何对方法返回的处理结果做类型判断和对注解FieldLimit的值的处理的主干过程,具体怎样构造新的Class,怎样创建新的对象,都在其中的handler中,也就是接口ReturnTypeHandler。方便后续的可插拔式处理。

现在ReturnTypeHandler支持处理

  1. 返回数组,例如UserVO[],对应的handlerReturnTypeCollectionHandler
  2. 返回集合,例如List<UserVO>,对应的handlerReturnTypeArrayHandler
  3. 返回单个POJO对象,例如UserVO,对应的handlerReturnTypePOJOHandler

如果你以后还想支持Map也可以再去添加一个ReturnTypeHandler实现类即可。不过你需要对于Javajava.lang.reflect.Type以及SpringResolvableType也一些了解才比较好写。

所有实现类都在这里github上,你可以慢慢看

当然当然,不知道刚刚第一次提到注解FieldLimit,我给予注解的@RetentionRetentionPolicy.RUNTIME,也就是运行时也保留,因此我下面的方案才是Spring AOP+注解,

但是我也在FieldLimit中提到,其实也可以是RetentionPolicy.SOURCE,也就是注解是在编译时才有效。那最终的方案就不是Spring AOP+注解,毕竟编译时那还没有Spring啥事呢。那就走我们的APT(Annotation Processing Tool)。就是Lombok类似,不过甲方的这里的情况要比Lombok的场景稍微难点(个人感觉),毕竟Lombok已经是偷偷修改了AST(抽象语法树),但是修改也是加方法而已,是增量,但是甲方这里的需求是直接需要修改以前的类的返回,自己以前也仅仅是做过新增类,就是auto-constants,它是根据POJO对象新增一个专门存放其属性名的类,这样就不用老写了死代码了,如果修改了属性,对应的地方不修改,是要报错的,这都还没有做到修改AST,但是给我的感觉是可以做的,不过估计要比我提供的Spring AOP+注解还要麻烦点,算是提供一种思路了

不过最后说下来,题主也可以看到,其实明明新增两个对应业务POJO对象就解决问题了,但是这个也太懒了叭。。。

关注 4 回答 3

imango 回答了问题 · 1月16日

解决每日百万数据量的订单表如何导出到excel

其实看了题主的问题以及其他回答下面题主的回答,我貌似get到了题主的想法

总得来说就是觉得数据已经存进去了,但由于导出excel,需要再循环查询数据读出来,感觉有点"多此一举",这个多此一举导致了循环访问数据库,对此感到不安

那我提供一种思路,不一定能解决你的问题,但是或多或少是一种思路叭

即:能不能在插入订单数据的同时也能发个通知,也就是做个消息中间件,这样消费者就是execl导出服务,读取消息直接此时就写入到excel

当然那肯定要准备一个消息中间件了,看你百万的数据量,肯定kafka没跑了,至于如何往kafka里扔消息,这里面有两种方案可以提供给你

  1. 在插入订单的代码中新增一个消息发布
  2. 做数据库的CDC(Change data capture)处理,也就是数据库的数据变更捕捉处理

方案1 那就是很简单啦,直接加代码,但是呢由于要在之前的逻辑里加东西,那就不好说了,毕竟开闭原则嘛,修改以前的逻辑势必会带来一些风险

方案2 这就基本不用改以前的代码了,毕竟做的是CDC,当然不知道你们用的什么数据库,那CDC的具体方案就不一样了,如果以mysql举例,当然是基于它的binlog来做处理咯,随便搜一搜一大堆,你可以自己造轮子,也可以用别人早已经造好的,当然造好的确实需要花一些代价去学习啦。不过呢不用硬生生修改之前的逻辑,主要就是去解析增量的数据库日志就可以了

当然也许你会问,主动去解析日志文件跟去循环查询数据库不是还是差不太多么?

no no no,有些数据库是支持响应式的操作的,就是你主动查和数据库主动推送给你的区别,比如解析mysql binlog的工具Maxwell,好家伙,它就是伪装成Slave,这不就可以从Master节点获取到binlog了嘛,从而再发出来,如果发给kafka,你再做一个消费者服务,不就可以了么

总之CDC是一种数据集成方法吧,不同数据库实现CDC的方法并不相同,当然其实早已经有一个库整合了不少主流数据库,然后集成了自己一套CDC的接口,你可以去看看debezium,不过这玩意儿,我自己装起来很头大,后面公司最终的方案也没有用debezium,还是找到了我们自己使用的数据库的官方响应API完成的

仅做参考吧,我也不知道对你有没有帮助,不过最后提一嘴,题主你提到:"频繁访问数据库,会严重影响数据库的性能"
影响性能根本原因就是读写操作是一个库嘛,所以你也可以考虑分库啊,读写分离啊,是吧,就是导出专门是一个只读从库就可以了嘛,和主库分开部署,这样即使你现在这样做,频繁访问数据库其实影响也不大,如果还有很大影响,说明你从库的机器配置太低了。。。叭

关注 4 回答 2

imango 回答了问题 · 2020-12-26

解决群发私信功能怎么制作?

方案一肯定是不行的
方案二看似可以的,但是题主自己觉得有问题,问题在于A关注的用户太多,所以A拉取的数据多...

但...如果我们随便看看咱们常用社交app的做法,加上针对一次数据过多的常用处理来说,两相结合一想,那方案也就出来了

一次数据过多常用处理,那就是分页嘛,所以首先对于A打开页面进行浏览时,拉取的数据肯定是要分页的。分页就可以保证一次性不会拉取较多的数据了

其次,如果光有分页可能还不行,考虑到社交属性,比如A关注的B,C,D,E,D,但B一天发100条动态,就算分页的话,那A打开页面,翻几次页都是B的动态肯定也是不行啦,所以这个时候肯定就必须要做关注人的动态分组

比如微博APP做法就是,你刷关注人动态时,基本是看到B的两条或三条动态,下面有个折叠按钮:查看该博主其他动态,点击就会再弹出几个该博主的动态,这是一种分组方式

再比如知乎APP,知乎那就很明显啦,知乎分组就在关注人页面的最上方,按照你关注的人显示的最经常访问

如果不分组,应该分页也就够了...不过我之前也是没做过类似需求哈,只是根据自己观察到的提的方案,抛砖引玉,期待做过的大佬再来分享,就酱!

关注 5 回答 4

imango 回答了问题 · 2020-12-08

java中有比较好的方式去获取某个月中星期几所对应的日期吗?

加7天的方式来说,是比较耿直的,但是这也是很顺畅并且正确的思路嘛,就算有java8封装好的一些api,它本质的实现逻辑肯定也还是逃不过这个天数的加减嘛,毕竟加减总比挨着挨着比要好点噻

话说回来,那我开始看到这里,我首先就想到了笨办法,挨着挨着比嘛。
(注:不过在列出我的代码前,我想额外提一点,我建议呢,这个方法的入参最好还是要加个年吧,毕竟2019的12月里的周六,和2020年的12月的周六,那肯定是不一样的。所以我下面给出的方法,入参都是年+月)

那思路就很简单啦,我们直接根据输入的年和月

  1. 构造出咱们的YearMonth(这个可是java8新增的年月的时间类,整挺好)
  2. 根据YearMonthlengthOfMonth()可以快速得到这个月多少天
  3. 依据这个月多少天,利用IntStream.rangeClosed()方法构造一个这个月所有天的数字流IntStream,再用mapToObject()方法转换成Stream<LocalDate>
  4. Stream<LocalDate>挨着挨着判断是否等于周六,最后collector(Collectors.toList())完成
public static List<LocalDate> querySaturday(int year, Month month) {
     YearMonth yearMonth = YearMonth.of(year, month);
     return IntStream.rangeClosed(1, yearMonth.lengthOfMonth())
                     .mapToObj(day -> LocalDate.of(year, month, day))
                     .filter(day -> day.getDayOfWeek() == DayOfWeek.SATURDAY)
                     .collect(Collectors.toList());
}

总之也还算比较简单,不过就是有点费空间,毕竟中间有很多不要的LocalDatenew出来了

那回到题主最早想的加减法方式,当然我们肯定不会自己去加减,不过思路其实和上面一样,肯定也是要构建一个流的,不过加减法的说法那是我们程序员最底层的思维方式,但是放在理解java8的很多api设计上,咱们需要的是声明式的思维,告诉api你想要什么,而不是告诉它怎么去实现,这样起码来说找方法你会想到一些关键词,也就是说其实我最开始也是不知道具体到底有没有api,一旦你多了解了一些java8api设计,不说怎么设计,起码它这个起名你就摸到点门道~

那关键词是什么么?是next,下一个,虽然最开始我不知道到底有没有api,但是我的思路是:
构造一个流,这个流的创建是基于下一个周六,这样的模式创建的流里都是周六,然后依据个数简单截断,再依据月份进行过滤,最终得到答案

那为啥我会想到是next呢?在调整时间上api是咱们的TemporalAdjuster接口掌管,一个TemporalAdjuster接口实现就是一种调整时间的方式,它很多的实现类都在已经为你写好的工具类TemporalAdjusters中,对,多了个s,这里面的静态构造方法名,你打开一看就明白了

image.png

尽都是些很直抒胸臆的方法命名,一看你就知道它在做啥。什么firstlastprevious,那我也就立马发现这个方法,恰好就叫next,传参就是DayOfWeek
image.png

DayOfWeek是啥?就是周一到周六的枚举啊,官方的
image.png

安排的明明白白,那就简单了,我们找到了如何获取下一个周六的方法,那我们只需要首先获取这个年月的第一个周六,然后依照TemporalAdjusters.next方法就可以构造出所有都是周六的流了

那我们怎么获取这个年月的第一个周六呢?还记得刚才说的么?那就找找first开头的方法呗,巧了,还真有一个firstInMonth,传参也是DayOfWeek,这不就简单了么,所以我们可以直接完整代码了

public static List<LocalDate> querySaturday(int year, Month month) {
    YearMonth yearMonth = YearMonth.of(year, month);
 LocalDate firstSaturday = LocalDate.now().with(yearMonth).with(TemporalAdjusters.firstInMonth(DayOfWeek.SATURDAY));
 return Stream.iterate(firstSaturday, localDate -> localDate.with(TemporalAdjusters.next(DayOfWeek.SATURDAY)))
              .limit(4)
              .filter(localDate -> localDate.getMonth() == month)
              .collect(Collectors.toList());
}

差不多就是这样叭,拜了个拜~

关注 3 回答 2

imango 回答了问题 · 2020-11-26

解决项目冗余代码怎么处理?

从之前的大家的回答和讨论来看,微服务是被题主否定了,这肯定是多方面的考虑嘛,并且微服务只是一种方案的选择而已,也并不是一招吃遍天下的,很显然之前代码的处理肯定是按照引用一个通用工具jar包调用的方式处理,所以改为微服务的话,就算不谈此次缓存和数据库的修改逻辑来说,光架构方面的改动,调用方和提供方改动都还是比较大的。还有双方开发人员对于微服务的熟悉程度。

所以我起初看到这个问题的时候,我反而还没有想用微服务的,因为从题主的描述来看,我感觉他更希望用一种结合Spring整合缓存,然后替换之前的工具包里的逻辑而已,这样相当于只是改了之前工具包里的逻辑,就算调用接口被改,调用方修改的代价也是比较小的,改改方法名和入参而已。

因此我谈谈我自己的看法吧,我能想到的Spring整合缓存那肯定是Spring CacheSpring作为一个整合框架,要的就是一招可以针对某个业务提出统一接口,然后第三方实现,相当于Spring项目级别的SPI,那对于Cache来说,Spring也有抽象有一套接口和注解,可以搜索直接Spring Cache,简单了解一下用法即可

回到题主的需求上,需要读数据库+redis+本地缓存,那其实我自己理解这应该就是本地缓存作为一级缓存,redis作为二级缓存,最后再去读DB

那这跟Spring Cache的方法注解@Cacheable就有类似的效果了

@Cacheable标注一个方法,提供缓存的name,对应的key,以及使用哪个CacheManager处理,例如下面的例子一样

@Cacheable(cacheNames = "DIRTY_DATA", key = "'all_dirty_data'", cacheManager = "CACHE_MANAGER")
public List<String> all() {
    return Arrays.asList("data1", "data2");
}

如果这次操作的CacheManager返回为null,那就执行方法all(),并且方法all()的值最后会被交给CacheManager中的Cache做处理缓存起来

如上的描述其实基本就满足了题主的需求,只是有一点,题主是有两级缓存的,刚刚的描述好像没有提到,其实不然,我们来看SpringCacheManager的接口定义

image.png

根据缓存的name返回一个Cache,其实Cache就是某种类型的缓存的抽象,比如有Redis的实现RedisCacheEhCache的实现EhCacheCache等等。

但是CacheManager的定义是根据name返回一个Cache,也没有指定要什么类型的(虽然RedisCacheEhCacheCache都有他们自己的CacheManager),所以CacheManager管理的Cache理论是可以有多不同类型的实现。当然不用题主定义,Spring想好了,请看CompositeCacheManager
image.png

CompositeCacheManager如其名,组合的CacheManager,虽然有不同类型的Cache实现,也有不同的CacheManager类型实现,现在CompositeCacheManager就可以把不同类型的CacheManager合在一起,因为合在一起的方式是集合List<CacheManager>,而集合是有顺序的,所以题主这下应该明白为啥可以实现两级缓存了叭,这就是我的想法

那废话不多说,只要题主了解一下Spring Cache的相关知识,接下来我的做法应该可以理解了
本地缓存我就以EhCache举例吧,我主要展示Cache的配法,其他的配置就不展示了哈

那我们先配置一个EhCacheCacheManager: EhCacheCacheManager

@Autowired
private CacheProperties cacheProperties;

@Bean
public EhCacheCacheManager ehCacheCacheManager() {
    Resource location = this.cacheProperties.resolveConfigLocation(this.cacheProperties.getEhcache().getConfig());
 return new EhCacheCacheManager(EhCacheManagerUtils.buildCacheManager(location));
}

接下来再配一个RedisCacheManagerRedisCacheManager

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
    RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
            .build();
 return cacheManager;
}

两个CacheManager都有了,关键的,我们再配置一个新的CacheManagerCompositeCacheManager

@Bean
@Primary
public CompositeCacheManager compositeCacheManager(EhCacheCacheManager ehCacheCacheManager, RedisCacheManager redisCacheManager) {
    return new CompositeCacheManager(ehCacheCacheManager, redisCacheManager);
}

注意上面的顺序哈,如果想要优先EhCache命中,当然EhCacheCacheManager就要放在构造方法的参数中的最前面哈,并且为了@Cacheable配置起来很简洁,我们加一个@Primary,因为现在有三个CacheManager了,这样的话,我们使用@Cacheable就可以不用指定那个cacheManager参数了

其次,为了CompositeCacheManager@Cacheable能关联起来,我们需要配置一个CacheResolver

@Bean
public CacheResolver cacheResolver(CompositeCacheManager compositeCacheManager) {
    return new SimpleCacheResolver(compositeCacheManager);
}

最后,就是如何使用了,假设对外提供了一个脏数据查询的接口

public interface IDirtyDataService {
    List<String> all();
}

那它的实现可以这样写

@Service
public class DirtyDataServiceImpl implements IDirtyDataService {
    @Override
    @Cacheable(cacheNames = {"EHCACHE_DIRTY_DATA", "REDIS_DIRTY_DATA"}, key = "'all_dirty_data'")
    public List<String> all() {
        return Arrays.asList("data1", "data2");
 }
}

非常简单哈,这其中的cacheNames中的EHCACHE_DIRTY_DATA,是和你配置的EhCache的名字一样哈,因为EhCache的配置是xml的,所以xml里的名字要和这里一致

image.png

至于RedisREDIS_DIRTY_DATA,这个可以随意了,毕竟没有的话,默认情况下,它是可以自己造一个的,参考RedisCacheManagergetMissingCache

image.png

我把完整代码放到github上吧,我提供的方案只是我自己对于你描述的业务的理解,虽然不一定能解决你的问题,不过Spring Cache是你可以考虑的方向哈

那就这样叭~拜了个拜(ˉ▽ ̄~)

关注 5 回答 5

imango 回答了问题 · 2020-11-25

解决xstream转换xml时出错

从报错来看是说找不到这个字段LINE_RECIPE,而这个字段的定义是在Body
image.png

一看这个字段在Body里的定义就感觉有点奇怪了,可能题主要理解一下这个两个注解的差别了

@XStreamAlias
@XStreamImplicit

尤其是@XStreamImplicit,如果能明白@XStreamImplicit的含义,其实也就豁然开朗了,主要在其中的Implicit这个单词,这个是隐式的意思,打开@XStreamImplicit注解也说得比较明白,人家就是处理隐式集合
image.png

那什么是隐式集合呢?看官方解释吧(图中有个小bug,应该是隐式,不是隐藏,打错了)

image.png

结合解释你再回过头看看,你就明白了,你的xml文件里的元素LINE_RECIPE_LIST实际就是一个显示的集合标志嘛,所以干嘛这时候要去用@XStreamImplicit呢?直接@XStreamAlias("LINE_RECIPE_LIST")就够了啊

所以改法的话,很简单,删除@XStreamImplicit(itemFieldName = "LINE_RECIPE")即可

 @XStreamAlias("LINE_RECIPE_LIST")
 List<LineRecipe> LINE_RECIPE_LIST;

当然你非要想使用@XStreamImplicit注解也行,那就把xml文件中的LINE_RECIPE变为隐式的集合即可嘛

怎么变呢?结合刚才官方那个截图,没有外面的集合标签就行啦,所以如果你想要写成

 @XStreamImplicit(itemFieldName = "LINE_RECIPE")
 List<LineRecipe> LINE_RECIPE_LIST;

xml文件就必须改为
image.png

当然你作为解析方,一般来说呢,比较卑微。。。估计没有权利让对方改接口,所以,只能用第一种方案,不过我额外这么一提,主要还是想让题主更清楚@XStreamImplicit的用法吧,希望能对你有所帮助

那...拜了个拜~(* ̄rǒ ̄)

关注 2 回答 1

imango 回答了问题 · 2020-11-22

解决java spring boot把一个类赋值给另一个类的属性,通过属性访问不了方法是怎么回事啊?

应该是方法访问权限问题,也就是countselectAll方法在你的控制器中无法访问到

两种可能吧

  1. 也许countselectAll都是private修饰符的方法
  2. 也许countselectAll是没有修饰符或者是protected修饰符的方法,并且控制器类和你的PlusServiceTKService不在同一个包

这个问题不是spring boot的知识点,是java基础知识哈,之后可以再去做了解哈java的访问权限

关注 2 回答 1

imango 回答了问题 · 2020-11-08

java css 选择器的问题

u1s1,开始你这句
image.png

我愣是弄了半天没读懂,看了@云香水识前端大哥的回答,我算是弄明白大体意思,应该就是要把a标签里的时间字符串全部提取出来,不用再用Elements挨着挨着去循环吧

我有印象你之前也问个几个问题,是关于爬虫的,所以虽然@云香水识可以提取到字符串数组,但是不是你要的,你是想用jsoup搞定吧

当然你先得用jsoup选择器定位到时间字符串那个a标签上,然后你需要了解Elements的一个方法eachText(),它可以把你讨厌的事情都做了
image.png

所以下面这样,就应该可以达到你要的效果叭

List<String> list = document.select("td[class='views-field views-field-date'] > a").eachText()

但愿没理解错意思~就酱~(@ ̄ー ̄@)

关注 3 回答 2

imango 回答了问题 · 2020-11-06

解决java 格式化报错

TNT的回复是没有问题的哈,我自己都试过了,看你评论说还有问题,我猜想你可能看得比较快,没有注意改全

如果你还是用printf,除了把%n改为%n%s,还有一点,很关键啊,就是要把link作为参数传入printf里啊,所以人家写的也是很清楚辣,中间是逗号,不是加号,不然加%s不就是占位符嘛
image.png

我估计你是写成了

System.out.printf("每个公司链接为:%n%s" + link);

你只用改这么一点就行啦,改完就没毛病啦,数据都打印出来了
image.png

关注 3 回答 2

imango 回答了问题 · 2020-11-05

elasticsearch 整合springboot 的时候出现了日期格式转换的问题 ,找了好多也没有解决,请大佬帮助

2020-11-04更新

这次直接整一个视频来,之前回答的稍微有点乱了,如果没看懂可以看看我录的解说视频叭(不过貌似声音有点小。。。)
https://www.bilibili.com/vide...

2020-10-16 更新

今天又要更新一下,昨天的回答最后不是提到我去spring data elasticsearch提了一个issues,还怕被那边大佬打脸说不是问题,经过昨晚大佬简短的确认问题,今天北京时间凌晨4点,大佬复现了问题,并且做了代码修改
image.png

也就是说,这确实是一个bug,而且修改方法也是按照我15号更新的方式去修改的,奈斯啊(这是spring data elasticsearch代码提交记录
image.png

也就是说String -> TemporalAccessor后一定要经历过一次再转ZonedDateTime之后,才能转成Instant,不过呢,spring data elasticsearch还没有发版本,所以想要同样的修改,还是可以按照15号我的方式做也行哦╰(_°▽°_)╯

2020-10-15 更新

昨天的回答,确实很牵强,毕竟不是按照题主的需求来做的修改,我今天再看了一下,终于自己确认了一些问题同时也暂时找到一个可以实现题主要求的方式

确认的问题:

spring data elasticsearch在处理实体属性类型为java.util.Dateread操作上有问题,也就是应该有bug,其实也就是在ElasticsearchDateConverter.parse的方法处理上
image.png

这个我在昨天初次回答的时候已经提到,有问题的地方就在于dateFormatter.parse(input)出来的结果TemporalAccessorInstant.from调用失败,但当时我没有仔细想,而是想办法去绕过,所以才有了昨天的回答

实现题主需求的方法:

我们忽略了一个问题就是,时间转换上,spring data elasticsearch其实都用的DateTimeFormatter,也就是无论是String -> Date 还是Date -> String都是采用DateTimeFormatter的方法,那既然能够写入,读出应该也是可以的,毕竟DateTimeFormatter可是jdk自带的时间格式化工具,不可能说它只实现了一半功能,所以说,现在读出不行,写入可以,说明spring data elasticsearch在使用DateTimeFormatter时,写入时传入的参数正确,读出时传入的参数错误,所以我们也就可以去比对一下它们两个过程到底怎么做的,有什么区别。首先现在我们理清spring data elasticsearch处理过程大体为:

写入:Date -> Instant(TemporalAccessor) -> String
读出:String -> Parese(TemporalAccessor) -> Instant(TemporalAccessor) -> Date

不过再一次仔细看完写入的代码发现,其实Instant(TemporalAccessor)不是直接到String
image.png

Instant(TemporalAccessor)还经历了一次DateFormatters.from
该方法是把一个TemporalAccessor转换为ZonedDateTime,再由ZonedDateTime转换成String,此时写入过程变为

写入:Date -> Instant(TemporalAccessor) -> ZonedDateTime(TemporalAccessor) -> String

这下也就释然了,为啥呢?因为java.util.Date在功能上就是和新的java8时间APIZonedDateTime差不多的,这样的转换也是合理的。

反过来看读出时并没有这一步操作啊,其实这就不合理了,为啥呢?比如es里存的时间格式恰好没有时分秒,只有年月日,那它该怎么从一个年月日转换为Date呢?必须要补齐时分秒啊,这一步操作没有的话,是不能把TemporalAccessor转化为一个Instant的,Instant可是一个时间戳

所以正确的读出的过程应该是这样:

String -> Parese(TemporalAccessor) -> ZonedDateTime(TemporalAccessor) -> Instant(TemporalAccessor) -> Date

所以如果我们能把ElasticsearchDateConverter.parse的处理改为下面这样就可以了
image.png

为了达到这个效果,我们捋捋,同时去寻找spring data elasticsearch的扩展能力。

  1. 首先是我们的目标位置ElasticsearchDateConverter没有托管给Spring,它属于SimpleElasticsearchPersistentProperty的一个处理过程new出来的
  2. SimpleElasticsearchPersistentProperty也没有托管给Spring,它属于SimpleElasticsearchMappingContext,终于这个Context是被托管的,且有可扩展点

ElasticsearchDataConfiguration中我们可以看到扩展点配置
image.png

所以我们只需要去

  1. 补一个SimpleElasticsearchMappingContextBean覆盖默认的,简单取名为CustomSimpleElasticsearchMappingContext
  2. CustomSimpleElasticsearchMappingContext中要使用新的CustomSimpleElasticsearchPersistentProperty,而不是之前的SimpleElasticsearchPersistentProperty
  3. 然后CustomSimpleElasticsearchPersistentProperty要使用的是CustomElasticsearchDateConverter而不是ElasticsearchDateConverter
  4. 最后CustomElasticsearchDateConverter中修改parse即可

不知道这里几个类如果没看源码可能会绕晕点,不过还不算复杂,只是线性的依赖,我简单梳理一个图描述一下修改的想法

image.png

由于三个类里面的东西比较多,我干脆甩到了github上,可以自行查阅

同时,由于我认为这也是一个bug,所以直接跑去spring data elasticsearch官网提了一个issues,我简单截一个图,其实就是用蹩脚的英语描述一下。。。

image.png

hhhhh,虽然不知道会不会被打脸。。。。后续他们有回答的话,我会再来更新。。。就酱(๑•̀ㅂ•́)و✧

2020-10-14回答

试了哈,确实有这样的问题,先说哈我最后牵强的解决方案...
你的createTime字段修改一下,改为

@Field( type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonFormat (shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private LocalDateTime createTime;

也就是

  1. format改为date_hour_minute_second,它是标准ISO 8601的时间表示方式,存入数据库的格式为yyyy-MM-dd'T'HH:mm:ss,也就是比你之前的多了个T
  2. 数据类型由Date改为LocalDatetime

自己demo了一下是可以的,你可以试试看

至于为啥之前你这样写报错,原因说起来稍微复杂一点,我也是看了才知道

首先从报错入手,Instant.from报错,这是还没有转成最终的Date就失败了
image.png

那继续看出错具体原因为,在当前类的getLong方法中,也就是当前类不支持Instant传给它的Field
image.png

而当前类就是Instant.from的入参TemporalAccessor,此时实现为Parsed,虽然Parsed它也是一个TemporalAccessor,跟我们平常了解到的LocalDateLocalDateTime稍有不一样,但是都是TemporalAccessor,也就是都是时间的一个访问方式,只是它侧重于时间数据被parse后的存储,我个人理解为就是一种中间过程类,并不适合我们用,是DateTimeFormatter解析日期字符串后的产物,然后我们再可以根据TemporalAccessor的特性,query出我们要的时间形式即可

也就是说只要用DateTimeFormatter解析再加上用Instant做转换,永远都会报错,那我们接着看源码,可以不可以转变这样的结果

那就是继续沿着堆栈继续往上看,来到SimpleElasticsearchPersistentProperty$1.read
image.png

这一行代码豁然开朗,因为这里有个重要的分支isTemporalAccessor
image.png

TemporalAccessor不用多说,是咱们的时间访问入口,所以这就可以很自然的接入到Java8的时间API里了

直接查看它的实现方式,很明显,用了TemporalAccessorfrom方法,而这个是根据type来的,不用多说,这个type肯定就是实体类里定义的类型了
image.png

既然如此,我们就可以根据Java8的时间类型+题主业务需要来选择具体的TemporalAccessor,那我首先想到的用LocalDateTime

一试结果还是报错
image.png

此时从这里看temporalParsed,所以根本原因就是Parsedquery方法返回的LocalDate为空,而进入Parsed.query
image.png

其实就是因为Parsed里的date属性为空,那按照之前我们处理Instant的方案来说,这也就改不了么?

不是的,因为此时情况不一样,我们回顾一下此时的情况
LocalDateTimefrom方法调用了一个DateTimeFormatter解析后的结果Parsed失败

???

为啥我会有问号,因为日常我用java8时间api解析时间,不就是这么做的么?

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
TemporalAccessor parse = dateTimeFormatter.parse("2020-10-14 11:12:39");
LocalDateTime localDateTime = LocalDateTime.from(parse);

写了一万遍了,突然说这个不行?显然不是啊,所以比对一下这个简单的处理过程,减去相同变量,根本原因那就是这个DateTimeFormatter

扫了一下源码,在DateFormatters.forPattern中藏着所有秘密,这里就是具体的pattern对应的DateFormatter,一大堆if else,太多了,简单截个小图吧
image.png

那咱们现在写的patternyyyy-MM-dd HH:mm:ss,根据这些if else,恰好是最后一个else

image.png

注意这里的DateTimeFormatterBuilder,根据后面的代码可知,这里的DateTimeFormatterBuilder创建出来的DateTimeFormatter为最终解析咱们createTimeDateTimeFormatter

比对下我之前没有问题的写法,走入DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
其实为:

new DateTimeFormatterBuilder()
            .appendPattern(pattern)
            .toFormatter()

spring data Elasticsearch里的为:

new DateTimeFormatterBuilder()
            .appendPattern(input)
            .toFormatter(Locale.ROOT)
            .withResolverStyle(ResolverStyle.STRICT))

也就是说它比我的多了一个Locale.ROOTResolverStyle.STRICT,通过简单的控制变量法把我之前的例子分别加上其中一个后,发现导致报错的是ResolverStyle.STRICT

这个ResolverStyle代表解析日期和时间的程度,默认的是STRICT也就是严格,而我之前的有没有选那就是SMART智能

但从源码看起来,是没有机会修改这个ResolverStyle,那我当时想到只能是改变一下我们自己了,也就是按照ISO 8601的标准日期和时间展示格式来存储,因为yyyy-MM-dd HH:mm:ss并不是标准的
image.png

那既然是标准的,那DateFormat肯定就不会是custom,根据elasticsearch官网的说明
image.png

我最终采用了date_hour_minute_second,之后再试了一下,就ok了

不过由于其实不算真正解决题主的问题,毕竟需求来说,最终数据库存储还是需要yyyy-MM-dd HH:mm:ss的,我暂时也没有想到更好的方法,只是根据现有的线索算是临时方案吧,如果有更好的,可以@我一下我,那拜了个拜︿( ̄︶ ̄)︿


(题外的话:其实为啥我很有兴趣看下去,是我之前也整理过java8时间api的一些东西,尤其是接口设计方面,我尝试用自己的理解去解析时间api,同时用自己的语言写出了《Java8 时间API及主要接口个人理解》,可以直接看第三部分Java8时间接口),当时只是对一些顶层接口有了点印象,不过今天其实还算是有很多收获,尤其是关于era(世纪),不过era我在刚的回答里没有提到,我的文章里其实也没有提到,但是这次最终为啥date_hour_minute_second的方式又可以了,其实跟era是或多或少有些关系的,因为最终date_hour_minute_second里时间的fieldChronoField.YEAR,而之前错误的为ChronoField.YEAR_OF_ERA

关注 7 回答 5

认证与成就

  • 获得 244 次点赞
  • 获得 24 枚徽章 获得 3 枚金徽章, 获得 10 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-10-23
个人主页被 4k 人浏览