Controller中为什么不能写@Transactional

1.背景

Controller指SpringMVC项目中用于定义接口信息的类,该类一般会被@Controller或@RestController等SpringMVC相关注解标记;
@Transactional指spring-tx包中定义的事务注解,被该注解标记的方法或类将成为一个整体,“同进同退”;
在开发过程中,注解是我们的神兵利器,但如果不恰当的使用将会造成严重的问题。 read more

Spring切面处理日志

业务场景:在web服务中添加日志,要求在刚进入方法和结束方法的时候打印开始和结束日志;进入方法好说,直接在第一行打印即可,但是return的时候可能会有很多分支,所以我们必须要在更上一层进行处理

实现步骤:
1.创建TestRequestBodyAdvice并实现RequestBodyAdvice接口,类上添加注解@ControllerAdvice
2.创建TestResponseBodyAdvice并实现ResponseBodyAdvice接口,类上添加注解@ControllerAdvice
3.修改其中的默认返回值;例如null改为对应值,false改为true(这个不要盲目的改)
4.引入logger类
5.在关键位置写入要打印的日志
具体代码如下:

@ControllerAdvice
public class TestRequestBodyAdvice implements RequestBodyAdvice {

    private Logger logger= LoggerFactory.getLogger(TestRequestBodyAdvice.class);

    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        return httpInputMessage;
    }

    public Object afterBodyRead(Object object, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        logger.debug("即将进入{}方法",methodParameter.getMethod().getName());
        return object;
    }

    public Object handleEmptyBody(Object object, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return object;
    }
}




@ControllerAdvice
public class TestResponseBodyAdvice implements ResponseBodyAdvice {

    private Logger logger= LoggerFactory.getLogger(TestResponseBodyAdvice.class);

    public boolean supports(MethodParameter methodParameter, Class aClass) {
        if (aClass.isAssignableFrom(MappingJackson2CborHttpMessageConverter.class)){
            return true;
        }
        return false;
    }

    public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        logger.debug("即将进入{}方法",methodParameter.getMethod().getName());
        return object;
    }
}
* 以上两个类并非只是打印日志的作用,他可以在所有请求的进入和返回时刻进行处理,例如进行简单的参数校验,或者查询为空时在此处理给前端一个默认返回值等

* 以上代码采用了适配器模式,通过supports函数来判断是否进入下面的函数进行逻辑处理

Spring实现数据字典翻译

在开始之前,首先我们要了解一个类:BeanPropertyWriter。
这个类是由SerializerFactory 工厂进行实例化的,其作用是对bean中的每个字段进行jackson操作的封装,其中封装了字段的一些元信息,和对此字段进行jackson序列化的操作。
采用Spring项目进行Web服务开发时,在获取到数据后,Spring会通过BeanPropertyWriter对数据进行jackson封装,将其转换为Json串。
如果我们需要在不影响逻辑的情况下对数据进行字典翻译,重写此类是较好的选择

字典翻译实现步骤:
1.实现获取字典的接口

public interface DictService{

    /**
    *key:字典类别
    *value:字典代码值
    *return:字典代码对应的value值
    Object getValueByKey(String key,String value);
}

2.新建注解便于对需要转换的字段进行区分

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DictConverter{

   String key() default "";
}

3.在需要翻译的字段上添加该注解


@DictConverter(key="TEST)//数据字典中配有字典项为TEST的字典值
private String item;

4.新建SpringUtil类

@Component
public class SpringUtil implements ApplicationContextAware{

   private static ApplicationContext applicationContext=null;

   @Override
   public void setApplicationContext(ApplicationContext applicationContext)throws BeansException{

   if(this.applicationContext=null){
     this.applicationContext=applicationContext;
    }
   }

   //获取applicationContext
   public static ApplicationContext getApplicationContext(){return applicationContext;}

   //通过name获取Bean
   public static Object getBean(String name){
     return getApplicationContext().getBean(name);
   }

   //通过class获取Bean
   public static <T> T Object getBean(Class<T> clazz){
     return getApplicationContext().getBean(clazz);
   }

   //通过name,以及Clazz返回指定的Bean
   public static <T> T Object getBean(String name,Class<T> clazz){
     return getApplicationContext().getBean(name,clazz);
   }
}

5.重写BeanPropertyWriter类(主要实现部分)
粘出BeanPropertyWriter的包名,在自己的工程下创建这个包
新建BeanPropertyWriter类,将jackson的源代码copy过来
声明刚才创建的DictService及注解
创建getDictService()用于获取service对象
找到serializeAsField方法

private DictService dictService;
private DictConverter dictConverter;

private DictService getDictService(){
  if(dictService==null){
    dictService=SpringUtil.getBean(DictService.class);
  }
  return dictService;
}
public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
        Object value = this._accessorMethod == null ? this._field.get(bean) : this._accessorMethod.invoke(bean);
        //数据字典翻译
        try{
          if(this._member.hasAnnotation(DictConverter.class)){
            dictConverter=this._member.getAnnotation(DictConverter.class);
            if(dictConverter!=null){
               value=getDictService.getValueByKey(dictConverter.key(),value.toString());
               if(value==null){
                  value = this._accessorMethod == null ? this._field.get(bean) : this._accessorMethod.invoke(bean);
               }
            }
         }
        }catch(Exception e){
           //此处可能因字段类型出现报错
           value = this._accessorMethod == null ? this._field.get(bean) : this._accessorMethod.invoke(bean);
        }

        //以下部分不做修改,此处省略
}
* 为提高字典转换速度,建议将DictService进行优化,将字典值存入缓存等,以提高页面响应

SpringBoot单元测试

在SpringBoot项目开发过程中,我们引用了大量的注解,这样导致我们在对其进行测试时需要首先对bean进行创建,那么简单的Test注解就无法实现了,这个时候加入其它注解协助实现bean的创建

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private Service service;

    @Test
    public void contextLoads() {
       service.test();
    }

}
* 在执行过程中可能会出现eurekaAutoServiceRegistration 创建bean异常,网上给出的错误原因是端口被占用,所以建议在进行单元测试的时候关掉对应服务
* 其实在报上述错误的时候我们已经拿到了想要的结果了,所以如果短时间内解决不掉这个错误,可以先不去处理

Spring集成MyBatis设置打印SQL日志

在开发过程中,如果使用MyBatis进行开发,sql是在xml文件进行维护的,我们无法通过DeBug拿到完整的执行sql;
在SpringBoot集成MyBatis的项目中,日志文件一般通过logback-spring.xml进行配置
在这个阶段,如果需要通过配置实现sql打印,则需要在与标签平级的位置添加如下信息:

<logger name="com.baomidou.mybatisplus" level="DEBUG"><!--引用的包路径,我这里引用的是MyBatisPlus-->
<logger name="java.sql.Connection" level="DEBUG">
<logger name="java.sql.Statement" level="DEBUG">
<logger name="java.sql.PreparedStatement" level="DEBUG">

Java使用SpringBoot应用配置文件

*配置文件yml和properties均可,以下以yml为例
1.设置配置文件
app:
service:
name: justin
id: 000
2.创建配置文件映射类

@Component//如报错请将此注解注释,因为此时出现了该bean被注册两次的情况
@ConfigurationProperties("app.service")
public class ServiceProperties {

    private String name;//注意该字段名要与配置文件中保持一致
    private String id;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

3.创建配置文件读取类并将值赋给对应服务

@Configuration
@EnableConfigurationProperties(ServiceProperties.class)
public class ServiceAutoConfiguration {

    @Autowired
    private ServiceProperties properties;

    @Bean
    public ServiceExecutor serviceExecutor(){
        return new ServiceExecutor(properties.getName(),properties.getId());
    }
}

4.创建并调用对应的服务类

public class ServiceExecutor {

    private final String serviceName;

    private final String serviceId;

    public ServiceExecutor(String serviceName, String serviceId) {
        this.serviceName = serviceName;
        this.serviceId = serviceId;
    }

    public void execute(){
        System.out.println("--模拟对参数进行了处理");
        System.out.println("serviceName:"+serviceName);
        System.out.println("serviceId:"+serviceId);
    }
}

@RestController
public class TestController {

    @Autowired
    private ServiceExecutor serviceExecutor;

    @RequestMapping("/test")
    public void test(){
        serviceExecutor.execute();
    }
}
@ConditionalOnProperty
/**
*该注解用于判断当前@Configuration是否有效
*使用方法:
*1.直接读取配置文件中的值:
**/
@ConditionalOnProperty(value="app.service.enabled")
/**
*2.//如果synchronize在配置文件中并且值为true
**/
@ConditionalOnProperty(name = "synchronize", havingValue = "true")
 
/**
*使用场景:
*在微服务开发中,一般会有一个服务用来存放公共的config配置,
*但是每个服务可能会有细微的区别,这时我们只需要将config服务中提供的引用false掉,重新写入自己的配置即可
**/

Spring Cloud版本选择

大版本目前主要有:
Angel版本对应Spring Boot 1.2.x
Brixton版本对应Spring Boot 1.3.x
Camden版本对应Spring Boot 1.4.x
Dalston 版本对应Spring Boot 1.5.x
Edgware 版本对应Spring Boot 1.5.x
Finchley 版本对应Spring Boot 2.0.x
Greenwich 版本对应Spring Boot 2.1.x
小版本:
SNAPSHOT: 快照版本,随时可能修改
M: MileStone,M1表示第1个里程碑版本,一般同时标注PRE,表示预览版版。
SR: Service Release,SR1表示第1个正式版本,一般同时标注GA(GenerallyAvailable),表示稳定版本。
生产系统建议选择GA稳定版本

D版本和E版本的区别
二者均基于SpringBoot的1.5.x版本。但支持其他组件的版本不同,每个小版本的不同,会有细微差别。如以 Dalston.SR4 和 Edgware.RELEASE 来对比:

spring-cloud-config 分别对应 1.3.3和 1.4.0;
spring-cloud-netflix 分别对应 1.3.5和 1.4.0;
spring-cloud-consul 分别对应 1.2.1和 1.3.0;
spring-cloud-gateway 前者不支持,后者 1.0.0。

版本的选择建议
考虑单体应用的Spring Boot的版本兼容性。
如果你项目需要和与老项目集成,以兼容为第一要务。

SpringBatch读取excel异常

摘取关键报错信息:
1.Failed to initialize the reader
2.Failed to find end of row/cell records
3.Step failure: the delegate Job failed in JobStep.

根据异常信息可以得出,引起异常的原因为读取文件时有空行或空列出现
此处以excel为例:
解决途径:
1.Ctrl+A查看覆盖范围是否超出有效值部分
例如有效列只有1列,Ctrl+A后覆盖了两列,那么第二列就是引起错误的原因
清空第二列的内容重新执行
2.如没有1中情况或1中无效
复制有效值到新建excel中并对新建excel进行批处理操作