作者归档:Rick

后台中的Convert转换

分类

后台中的Convert,我总结了如下分类:

  1. springmvc自定义属性编辑器PropertyEditor

    我们在使用SpringMVC时,常常需要把表单中的参数映射到我们对象的属性中,我们只要使用默认的配置即可做到普通数据类型的转换,如将String转换成Integer和Double等。但如果我要把String映射到对象上,如“rick-18”映射到Student对象,就必须自定义converter。值得注意的是,这里的提交方式不能是raw(application/json)的 。只能是x-www-form-urlencoded/form-data类型

  2. springmvc自定义Converter

    Converter能完成属性编辑器的功能,而且更加通用,不仅仅可以接收UI参数。是Spring推荐的实现方式。关于两者的比较,可以查看https://stackoverflow.com/questions/12544479/spring-mvc-type-conversion-propertyeditor-or-converter

  3. stringmvc自定义HttpMessageConverter

    HTTP消息转换,比如可以让spring接收Content-Type:xx/yy类型,body是字符串“rick-18”,后台用对象Student接收。自定义raw。spring提供了很多默认的转换,如FormHttpMessageConverter,MappingJackson2HttpMessageConverter

  4. hibernate自定义属性

    hibernate在持久化,如果是自定义属性,该如何处理映射呢?如,有个属性是Student对象,希望能够将Student的json对象存储varchar到列stu上

  5. hibernate validation自定义验证方法

    如何自定义validation,验证逻辑错误。

实现

所有实现都是基于Springboot2.0,VEHICLE_BRAND#NS 映射到 Word

  • ## PropertyEditor
  1. 定义属性编辑器
    WordEditor.java
public class WordEditor extends PropertyEditorSupport {
    private static final String PARAM_SEPARATOR = "#";

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        if (StringUtils.hasText(text)) {
            String[] values = text.split(PARAM_SEPARATOR);


            Word word = new Word();
            word.setCategory(values[0]);
            word.setName(values[1]);
            setValue(word);

        } else {
            setValue(null);
        }
    }

    @Override
    public String getAsText() {
        Word word = (Word) getValue();
        if (word != null) {
            return word.getCategory() + "#" + word.getName();
        } else {
            return "";
        }
    }
}
  1. 全局绑定属性编辑器

PropertyConfiguration.java

@RestControllerAdvice
public class PropertyConfiguration {

    @InitBinder
    public void registerCustomEditors(WebDataBinder binder) {
        binder.registerCustomEditor(Word.class, new WordEditor());
    }
}

当然,可用在单独的Controller中绑定

  • ## Converter
  1. 定义Converter

StringToWordConverter.java

@Component
public class StringToWordConverter implements Converter<String, Word> {
    private static final String PARAM_SEPARATOR = "#";

    @Override
    public Word convert(String s) {
        String[] values = s.split(PARAM_SEPARATOR);


        Word word = new Word();
        word.setCategory(values[0]);
        word.setName(values[1]);
        return word;
    }
}

注意:Converter是来自接口org.springframework.core.convert.converter.Converter

  1. 添加Converter
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToWordConverter());
    }
}
  • ## HttpMessageConverter
  1. 定义 HttpMessageConverter

WordHttpMessageConverter.java

@Component
public class WordHttpMessageConverter extends AbstractHttpMessageConverter<Word> {
    private static final String PARAM_SEPARATOR = "#";

    public WordConverter() {
        super(new MediaType("text", "word"));
    }

    @Override
    protected boolean supports(Class<?> aClass) {
        return Word.class.isAssignableFrom(aClass);
    }

    @Override
    protected Word readInternal(Class<? extends Word> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        String s = toString(httpInputMessage.getBody());

        String[] values = s.split(PARAM_SEPARATOR);


        Word word = new Word();
        word.setCategory(values[0]);
        word.setName(values[1]);
        return word;
    }

    @Override
    protected void writeInternal(Word word, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        OutputStream outputStream = httpOutputMessage.getBody();
        String body = word.getCategory() + "#" + word.getName();
        outputStream.write(body.getBytes());
        outputStream.close();
    }

    private static String toString(InputStream inputStream) {
        Scanner scanner = new Scanner(inputStream, "UTF-8");
        return scanner.useDelimiter("\\A").next();
    }

}

前端可用通过Content-Type: text/word,进行消息转换

  1. 添加MessageConverter
@Configuration
public class OAWebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new WordConverter());
    }
}
  • ## hibernate Converter
  1. 新建Converter

WordJpaConverter.java

@Convert
public class WordJpaConverter implements AttributeConverter<Word, String> {

    private static final String PARAM_SEPARATOR = "#";

    @Override
    public String convertToDatabaseColumn(Word word) {
        if (Objects.isNull(word)) return "";

        StringBuilder sb = new StringBuilder();
        return sb.append(word.getCategory()).append(PARAM_SEPARATOR).append(word.getName()).toString();
    }

    @Override
    public Word convertToEntityAttribute(String s) {
        if (StringUtils.hasText(s)) {
            String[] values = s.split(PARAM_SEPARATOR);

            Word word = new Word();
            word.setCategory(values[0]);
            word.setName(values[1]);
            return word;

        }
        return null;
    }
} 
  1. 使用Converter

Vehicle.java

    @Entity
    public class Vehicle {
        @Id
        private Long id;

        @Convert(converter = WordJpaConverter.class)
        private Word vehicleBrand;
    }
  • ## hibernate validation
  1. 添加注解DictionaryConstraint.java
@Documented
@Constraint(validatedBy = DictionaryValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DictionaryConstraint {
    String message() default "Invalid dictionary data";
    String name();
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  1. 设置验证逻辑DictionaryValidator.java
public class DictionaryValidator implements ConstraintValidator<DictionaryConstraint, Word> {

    private DictionaryConstraint constraintAnnotation;


    @Override
    public void initialize(DictionaryConstraint constraintAnnotation) {
        this.constraintAnnotation = constraintAnnotation;
    }

    @Override
    public boolean isValid(Word word, ConstraintValidatorContext constraintValidatorContext) {
        String category = constraintAnnotation.name();

        if (Objects.nonNull(category) && !Objects.equals(category, word.getCategory())) return false;

        DictionaryService dictionaryService = Global.applicationContext.getBean(DictionaryService.class);

        Word _word = dictionaryService.findByCategoryAndName(category, word.getName());

        return Objects.nonNull(_word);
    }
}
  1. 使用验证
    Vehicle.java
    @Entity
    public class Vehicle {
        @Id
        private Long id;

        @DictionaryConstraint(name = "VEHICLE_BRAND")
        @Convert(converter = WordJpaConverter.class)
        private Word vehicleBrand;
    }

前端数据的提交方式

提交数据body到后台有如下几种方式
* form-data
* x-www-form-urlencoded
* raw
– application/json
– application/xml
– text/plain
– text/html
– …
* binary

分类可用参考Postman

分类说明

Type 描述 example
x-www-form-urlencoded 一个普通的表单提交或者url地址栏参数提交;适用post,get方式 key=1&key=2
form-data 表单POST提交,设置enctype="multipart/form-data" POST / HTTP/1.1-----------------------------735323031399963166993862150Content-Disposition: form-data; name=”text1″
raw:application/json 通过ajax提交,Content-Type设置application/json,会绑定到Header中提交,这个类型可用自定义xx/yy,这需要后端的支持 {“name”:”rick”}

参考链接
https://unifaceinfo.com/docs/0907/Uniface_Library_HTML/ulibrary/MIMETypes_773A6E5F2234453F9A878E3961418FC0.html\
https://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean

springboot+hibernate开发中的坑

1. Springboot2.0 限制文件上传的配置:百度都是针对Springboot1.x的.

# MULTIPART (MultipartProperties)
spring.servlet.multipart.enabled=true # Whether to enable support of multipart uploads.
spring.servlet.multipart.file-size-threshold=0 # Threshold after which files are written to disk. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.
spring.servlet.multipart.location= # Intermediate location of uploaded files.
spring.servlet.multipart.max-file-size=1MB # Max file size. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.
spring.servlet.multipart.max-request-size=10MB # Max request size. Values can use the suffixes "MB" or "KB" to indicate megabytes or kilobytes, respectively.
spring.servlet.multipart.resolve-lazily=false # Whether to resolve the multipart request lazily at the time of file or parameter access.

其他配置可以查看官网

2. SpringBoot内嵌的Tomcat对上传文件有限制,会切断Responese的响应。

需要自己创建新的ConnectorsEnable Multiple Connectors with Tomcat

3.swagger2碰到 @manytomany,启动会出现错误

Springfox 2.7.0 with M:n-Relation; Failed to start bean 'documentationPluginsBootstrapper'; nested exception is com.google.common.util.concurrent.ExecutionError: java.lang.StackOverflowError

暂时没有办法解决:官方是这么回复

4.jackson碰到@manytomany双向关联,response会出现循环引的溢出

同Jackson的注解,允许一方管理关系。通过注解@JsonManagedReference``@JsonBackReference
参考详细
http://www.sonymoon.com/archives/92

5. 使用注解@JsonManagedReference,@JsonBackReference,前台提交application/json;charset=UTF-8,后台会有错误

org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported

通过注解@JsonIgnore,将其中一方忽略 

6. @PutMapping不能获取参数,为null

@PutMapping获取参数的方式和PostMapping不一样

## 7. 在继承QuartzJobBean的任务类中,如果使用类JPA
实体类是@ManyToMany,注意user.getOrganizations。不能注入Service

“`java
public class SampleJob extends QuartzJobBean {

private String name;

// Invoked if a Job data map entry with that name
public void setName(String name) {
    this.name = name;
}

@Override
protected void executeInternal(JobExecutionContext context)
        throws JobExecutionException {

    UserService userService = Global.applicationContext.getBean(UserService.class);
    User user = userService.findById(1);

    System.out.println(String.format("Hello %s! time is %s", this.name,
            new SimpleDateFormat("HH:mm:ss").format(new Date())) + " -> " + user.getOrganizations());
}

}
异常信息
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.yodean.oa.sys.user.entity.User.organizations, could not initialize proxy – no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:582) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:201) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:561) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:132) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
at org.hibernate.collection.internal.PersistentSet.toString(PersistentSet.java:299) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
at java.lang.String.valueOf(String.java:2994) ~[na:1.8.0_111]
at java.lang.StringBuilder.append(StringBuilder.java:131) ~[na:1.8.0_111]
at com.yodean.oa.common.job.SampleJob.executeInternal(SampleJob.java:33) ~[classes/:na]
at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:75) ~[spring-context-support-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[quartz-2.3.0.jar:na]
at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [quartz-2.3.0.jar:na]

2018-03-23 17:43:50.952 ERROR 77460 — [ryBean_Worker-1] org.quartz.core.ErrorLogger : Job (TASK.t1 threw an exception.“`

[http://codrspace.com/Khovansa/spring-quartz-with-a-database/](http://codrspace.com/Khovansa/spring-quartz-with-a-database/)
[https://stackoverflow.com/questions/6990767/inject-bean-reference-into-a-quartz-job-in-spring](https://stackoverflow.com/questions/6990767/inject-bean-reference-into-a-quartz-job-in-spring)
[https://stackoverflow.com/questions/32336152/spring-boot-quartz-lazyinitializationexception](https://stackoverflow.com/questions/32336152/spring-boot-quartz-lazyinitializationexception)

## 8. jackson标签`@JsonIgnore`
* 我之前认为response的时候只是不会打印出去。
* 但是我发现,有注解`@JsonIgnore`的字段,使用`@RequestBody`,接受参数有问题。**接受不到**

## 9. SpringMVC @RequestBody 用map接收请求参数的问题解决
用`@RequestParam`,然后就对了。。对了。。了。。**这个注解必须要写**
```java
 @GetMapping("/tree")
    public Result<List<TreeNode>> getOrgsAsTree(@RequestParam Map<String, Object> params) {

    }

Set<Integer> List<Integer>接收参数都必须加注解@RequestParam
JavaBean,Integer[]等基本类型可以不用加

集合类不写注解会抛出异常

No primary or default constructor found for interface java.util.Set

10 JPA扩展Repository

http://www.baeldung.com/spring-data-jpa-method-in-all-repositories

注意在实现方法上,必须使用注解@Transactional
不然保存会失败

@Transactional
    public void updateNonNull(T t) {

        try {
            ID id = (ID)PropertyUtils.getProperty(t, "id");
            T persist = findById(id).get();
            NullAwareBeanUtilsBean.getInstance().copyProperties(persist, t);
            save(persist);
        } catch (Exception e) {
            throw new OAException(ResultEnum.UNKNOW_ERROR);
        }
    }

11 JPA 实体类最佳实践

如果字段名和数据库列名不对应,要显示地写出@column

   @Column(name = "user_id")
    private Integer userId;

不然在使用注解,无法正常启动

uniqueConstraints = {@UniqueConstraint(columnNames={"category", "category_id", "user_id", "del_flag"})}

12 用List Set代替数组

13 Entity类中用String代替Character

14 SpringBoot 消息推送(SSE)

spring封装了对sse的操作
https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/sse-emitter.html

15 文件预览

office在线预览: https://blogs.office.com/en-us/2013/04/10/office-web-viewer-view-office-documents-in-a-browser/
office 转pdf -> mozilla/pdf.js 查看pdf文件

  • 如何word转pdf
    1. 安装libreoffice
      yum install libreoffice
    2. 执行命令
soffice --headless --invisible --convert-to pdf /mnt/a.docx --outdir /mnt

参考链接http://bakcom.iteye.com/blog/2397446
* 乱码解决方案

参考链接:https://blog.csdn.net/wlwlwlwl015/article/details/51482065

第一个子元素的margin-top会顶开父元素与父元素相邻元素的间距

今天遇到一个css,奇怪的问题:

第一个子元素的margin-top会顶开父元素与父元素相邻元素的间距,why?

<!DOCTYPE html>
<html lang="en">
<head>

    <style>
        .pat {
            height: 100px;
            background-color: red;
        }
        h1 {
            margin-top: 30px;
        }
    </style>
</head>
<body>
    <div class="pat">
        <h1>hello man</h1>
    </div>
</body>
</html>

div没有在最顶部显示,反而是h1置顶了。如图:

http://xhope.top/wp-content/uploads/2018/01/1111.png

这个问题发生的原因是根据规范,一个盒子如果没有上补白(padding-top)和上边框(border-top),那么这个盒子的上边距会和其内部文档流中的第一个子元素的上边距重叠。

再说了白点就是:父元素的第一个子元素的上边距margin-top如果碰不到有效的border或者padding.就会不断一层一层的找自己“领导”(父元素,祖先元素)的麻烦。只要给领导设置个有效的 border或者padding就可以有效的管制这个目无领导的margin防止它越级,假传圣旨,把自己的margin当领导的margin执行。
对于垂直外边距合并的解决方案上面已经解释了,为父元素例子中的middle元素增加一个border-top或者padding-top即可解决这个问题。

参考链接http://www.hicss.net/do-not-tell-me-you-understand-margin/

logback.xml基本配置

由于版本不一样,logback的配置总存在差异,导致不能正常使用。

我的版本

slf4j: 1.7.25
logback: 1.2.3
参考官网:https://logback.qos.ch/documentation.html

<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="true">
    <!-- 定义日志的根目录 -->
    <property name="LOG_HOME" value="/Users/rick/jkxyx205/log" />
    <!-- 定义日志文件名称 -->
    <property name="appName" value="site"></property>
    <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!--
        日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度
        %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
        -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
    <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定日志文件的名称 -->
        <file>${LOG_HOME}/${appName}.log</file>
        <!--
        当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
        TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
        -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--
            滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动
            %i:当文件大小超过maxFileSize时,按照i进行文件滚动
            -->
            <fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!--
            可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
            且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
            那些为了归档而创建的目录也会被删除。
            -->
            <MaxHistory>365</MaxHistory>
            <!--
            当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
            -->
            <maxFileSize>100MB</maxFileSize>
            <totalSizeCap>20GB</totalSizeCap>

        </rollingPolicy>
        <!--
        日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符
        -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
        </encoder>
    </appender>

    <!--
    logger主要用于存放日志对象,也可以定义日志类型、级别
    name:表示匹配的logger类型前缀,也就是包的前半部分
    level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
    additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,false:表示只用当前logger的appender-ref,true:表示当前logger的appender-ref和rootLogger的appender-ref都有效
    -->
    <!-- hibernate logger -->
    <logger name="org.hibernate" level="ERROR" />
    <!-- Spring framework logger -->
    <logger name="org.springframework" level="ERROR" additivity="false"></logger>

    <logger name="com.yodean.site" level="DEBUG" additivity="true">
        <!--<appender-ref ref="appLogAppender" />-->
    </logger>

    <!--
    root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
    要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。
    -->
    <root level="INFO">
        <appender-ref ref="stdout" />
        <appender-ref ref="appLogAppender" />
    </root>
</configuration>