字数:21669

1 引言

springboot的一个常规示例,主要包含几点功能:

  • 2.1 spring boot main启动

  • 2.2 统一的异常处理

  • 2.3 基类entity

  • 2.4 表单form bean与entity bean分开

  • 2.5 Json工具类

  • 2.6 redis template

  • 2.7 mqtt集成

  • 2.8 swagger2集成

  • 2.9 spring data jpa介绍

  • 2.10 几个小配置

  • 2.11 @JsonView的应用

  • 2.12 @JsonProperty的应用

  • 2.13 统一加返回信息的code/error message

  • 2.14 @Cacheable的应用

代码结构如下:

└─com
    └─xx
        ├─exception
        │      CustomizedResponseEntityExceptionHandler.java #集中异常处理
        │      ErrorDetails.java #异常信息自定义
        └─qingdu
            │  Starter.java #main启动
            ├─bean
            │  │  AbsIdEntity.java #抽象entity bean
            │  │  LibraryCard.java #具体entity bean
            │  │  LibraryCardExt.java #具体entity bean
            │  │  UserBase.java #具体entity bean
            │  │  UserByPhone.java #具体entity bean
            │  │  UserByWechat.java #具体entity bean 
            │  │
            │  └─frm
            │          FrmLibraryCard.java #form bean
            │          FrmNewLibraryCard.java #form bean
            │          FrmUserByWechat.java #form bean
            ├─comm
            │      CommConst.java #公共参数
            │      JsonUtil.java #json工具类
            │      Utils.java #工具类,主要是集中注入redis/mqtt/id generator等
            ├─config
            │      IdGeneratorConfig.java #ID generator实例化
            │      JpaConfig.java #spring data jpa实例化配置
            │      MqttConfig.java #mqtt实例化
            │      RedisConfig.java #redis实例化
            │      Swagger2Configuration.java #swagger实例化
            ├─controller
            │      LibraryCardController.java #控制类
            │      UserController.java #控制类
            ├─repository
            │  │  LibraryCardExtRepository.java #DAO接口
            │  │  LibraryCardRepository.java #DAO接口
            │  │  UserBaseRepository.java #DAO接口
            │  │  UserByPhoneRepository.java #DAO接口
            │  │  UserByWechatRepository.java #DAO接口
            │  │
            │  └─base
            │          BaseRepository.java #基础DAO接口
            │          BaseRepositoryFactoryBean.java #基础dao接口实例化工厂
            │          BaseRepositoryImpl.java #基础DAO接口的实现类,覆盖了上层接口的方法
            └─service
                │  LibraryCardService.java #服务接口
                │  UserByPhoneService.java #服务接口
                │  UserByWechatService.java #服务接口
                ├─impl
                │      LibraryCardServiceImpl.java #服务接口实现类
                │      UserByPhoneServiceImpl.java #服务接口实现类
                │      UserByWechatServiceImpl.java #服务接口实现类
                └─mqtt
                        MqttMessageConsumer.java #mqtt接收消息干活类
                        MqttMessageProducer.java #mqtt发送消息干活类

2.1 spring boot main启动

只要加上关键注解即可


@SpringBootApplication //main方式启动
public class Starter {	
	public static void main(String[] args) {
        SpringApplication.run(Starter.class, args);  
    }
}

2.2 统一的异常处理

要继承ResponseEntityExceptionHandler,参考这里

@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
	 
	 @Override
	  protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
	      HttpHeaders headers, HttpStatus status, WebRequest request) {
	    ErrorDetails errorDetails = new ErrorDetails(new Date(), "Validation Failed",
	        ex.getBindingResult().toString());
	    return new ResponseEntity(errorDetails, HttpStatus.BAD_REQUEST);
	  } 
}

更多的异常统一处理方式见这里

2.3 基类entity

为什么要基类entity,因为我打算统一处理id、新建、更新时间戳、逻辑删除,如果后续mysql -> elasticsearch,同步时或许需要用到更新时间戳。

@MappedSuperclass
public abstract class AbsIdEntity implements Serializable {   
	@Id
    @Column(name="id")
    protected long id;

    @Column(name = "create_time")
    protected Timestamp createTime;

    @Column(name = "last_time")
    protected Timestamp lastTime;

    @Column(name = "is_delete")
    protected int isDelete;
}

2.4 表单form bean与entity bean分开

form bean和entity bean分开,会造成写大量的form bean,为什么分开?原因是:

  • form bean的用法基本上是,现有数据entity bean的同名属性,如果在form bean中有值则覆盖
    后来在实践中有两条比较方便的做法:
    (1)即便两者都有属性,也可以设定不覆盖, 通过BeanUtils.copyProperties
    (2)com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper().readValue(json, cls)在json串转对象时,对象有多的属性也没OK

  • entity bean的属性更丰富,依照第1条,如果被Form bean滥用,关键数据可能会不安全
    后来在实践中有办法:即2.11,用JsonView注解

  • 在用swagger生成参数帮助时,它根据bean来生成json,这又加剧了第2条发生的概率
    后来在实践中有解决办法:**防止Formbean的属性把entity bean的覆盖?**用BeanUtils.copyProperties(userMini, exists, new String[] {"id", "idUnion", "createTime", "isDelete"}), 第三个参数就是解决哪些字段不要拷贝。

2.5 Json工具类

这个工具类的主要作用就是:

  • 将json串与对象互转,目前在springmvc的框架下,这个都不怎么用得上了,当然在与第三方接口交互时存在需求。

  • 另一点就是替换json串的字段值,为什么?因为在使用MQTT发消息时,消息用的是JSON模板,模板里的内容还是要适时变化一下的。

此工具类的内容如下,参考自:这里

public class JsonUtil {
    private static ObjectMapper mapper = new ObjectMapper();

    static {
        mapper.setSerializationInclusion(Include.NON_NULL);
        //设置输入时忽略JSON字符串中存在而Java对象实际没有的属性
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }
    
    public static void main(String[] args) {
    	User u = new User();
    	u.setName("tyc");
    	u.setAge(10);
    	
    	System.out.println(JsonUtil.object2Json(u));    	
    	System.out.println(JsonUtil.json2Object("{\"name\":\"daniel\",\"age\":12,\"notexists\":true}", User.class));
    	System.out.println(JsonUtil.replaceFieldsOfJsonstr("{\"name\":\"daniel\",\"age\":12,\"notexists\":true}"
    			, new String[][] {{"name","daniel1"}, {"age", "11"}, {"kk", "22"}}));
	}
    
    static class User{
    	String name;
    	String pwd;
    	int age;
    	
		public int getAge() {
			return age;
		}
		public void setAge(int age) {
			this.age = age;
		}
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
		public String getPwd() {
			return pwd;
		}
		public void setPwd(String pwd) {
			this.pwd = pwd;
		}
		
		@Override
		public String toString() {			
			return String.format("name:%s, pwd:%s, age:%d",
					name == null ? "" : name,
					pwd == null ? "" : pwd,
					age);
		}
    }
    
    @SuppressWarnings("unchecked")
	public static Map<String, String> jsonstr2Map(String jsonStr) {
    	try {
			return mapper.readValue(jsonStr, HashMap.class);
		} catch (IOException e) {
			e.printStackTrace();
		}
    	return null;
    }
    
    /**
     * 替换json string中的字段值
     * 
     * https://blog.csdn.net/qq_37936542/article/details/79268402
     * @param jsonStr 串
     * @param keyVal 键值对的二维数组
     * @return
     */
    public static String replaceFieldsOfJsonstr(String jsonStr, String[]... keyVals) {
    	if (keyVals == null || keyVals.length == 0) return jsonStr;
    	
    	try {
    		@SuppressWarnings("unchecked")
			HashMap<String, String> map = mapper.readValue(jsonStr, HashMap.class);
    		
    		for (String[] keyVal : keyVals) {
    			
    			if (map.containsKey(keyVal[0])) {
    				map.put(keyVal[0], keyVal[1]);
    			}
    		}
    		return mapper.writeValueAsString(map);
		} catch (IOException e) {
			e.printStackTrace();
		}

    	return null;
    }

    /**
     * 将对象转化成json string
     * @param o 对象
     * @return
     */
    public static String object2Json(Object o) {
        if (o == null)
            return null;

        String s = null;

        try {
            s = mapper.writeValueAsString(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return s;
    }

    /**
     * 将对象列表转化为json string列表
     * @param objects 对象列表
     * @return
     */
    public static <T> List<String> listObject2ListJson(List<T> objects) {
        if (objects == null)
            return null;

        List<String> lists = new ArrayList<String>();
        for (T t : objects) {
            lists.add(JsonUtil.object2Json(t));
        }

        return lists;
    }

    /**
     * 将json string列表转化为对象列表
     * @param jsons json string列表
     * @param c 类
     * @return
     */
    public static <T> List<T> listJson2ListObject(List<String> jsons, Class<T> c) {
        if (jsons == null)
            return null;

        List<T> ts = new ArrayList<T>();
        for (String j : jsons) {
            ts.add(JsonUtil.json2Object(j, c));
        }

        return ts;
    }
    

    /**
     * 将json string转化为对象 
     * @param json 
     * @param c
     * @return
     */    
    public static <T> T json2Object(String json, Class<T> c) {
        if (StringUtils.isEmpty(json))
            return null;

        T t = null;
        try {
            t = mapper.readValue(json, c);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return t;
    }

    @SuppressWarnings("unchecked")
    public static <T> T json2Object(String json, TypeReference<T> tr) {
        if (StringUtils.isEmpty(json))
            return null;

        T t = null;
        try {
            t = (T) mapper.readValue(json, tr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T) t;
    }
}

2.6 redis template

这个配置类目前还未完成,大概内容,比较奇怪的一点是实例化ReidsTemplate对象的代码注释后,还是可用:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
	
	//缓存管理器,扩展
	@Bean 
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheManager cacheManager = RedisCacheManager.create(factory);
        //设置缓存过期时间 
        //cacheManager.setDefaultExpiration(10000);
        return cacheManager;
    }
		
	/*//缓存管理器,扩展
	@Bean 
    public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        //设置缓存过期时间 
        cacheManager.setDefaultExpiration(10000);
        return cacheManager;
    }*/
	
	@Bean
	@Override
	public KeyGenerator keyGenerator() {
		//未解决的问题:缓存中的内容如何同步更新?
		return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(".").append(method.getName()).append("_"); //不能用冒号?
                
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
	}
	
	/*@Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
        //设置序列化工具        
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        
        @SuppressWarnings({ "rawtypes", "unchecked" })
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        StringRedisTemplate template = new StringRedisTemplate(factory);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }*/
}

用法就是在uitls统一注入:


@Component
public class Utils {
	@Autowired private SnowflakeIdWorker idGenerator;
	@Autowired private RedisTemplate<String, String> redisTemplate;
	@Autowired private MqttMessageProducer mqttMessageProducer;
	
	public SnowflakeIdWorker idGenerator() {
		return idGenerator;
	}	
	
	public MqttMessageProducer mqttProducer() {
		return this.mqttMessageProducer;
	}
	
	public RedisTemplate<String, String> redisTemplate(){
		return redisTemplate;
	}
	
	public <T extends AbsIdEntity> T createEntity(Class<T> cls) {
		T entity = null;
		try {
			entity = cls.newInstance();
			entity.setId(idGenerator.nextId());
			entity.setCreateTime(new Timestamp(System.currentTimeMillis()));		
		} catch (InstantiationException | IllegalAccessException e) {
			e.printStackTrace();
		}
		return entity;
	}
	
	/**
	 * 将缓存中的内容取出,并转化为bean返回
	 * @param key cache中键
	 * @param cls bean类
	 * @return
	 */
	public  <T extends AbsIdEntity> T getEntityFromCache(String key, Class<T> cls) {
		String jsonStr = redisTemplate.opsForValue().get(key);
		
		if (jsonStr != null) {
			return JsonUtil.json2Object(jsonStr, cls);
		}
		return null;
	}
}

2.7 mqtt集成

mqtt是一个消息框架,springboot中可以很方便地集成,引入依赖:

<!-- mqtt -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-mqtt</artifactId>
</dependency>

初始化方法如下:

@Configuration
@IntegrationComponentScan
public class MqttConfig {
	//为什么要定义两个id(serverId/clientId)? 
	@Value("${spring.mqtt.server.id}")
    private String serverId;
	
	@Value("${spring.mqtt.username}")
    private String username;

    @Value("${spring.mqtt.password}")
    private String password;

    @Value("${spring.mqtt.url}")
    private String hostUrl;

    @Value("${spring.mqtt.client.id}")
    private String clientId;

    @Value("${spring.mqtt.default.topic}")
    private String defaultTopic;
    
    @Autowired private MqttMessageConsumer mqttMessageConsumer;
    //-----------------consumer    
	
    //channel adapter通道适配器是单向的,入站通道适配器只支持接收消息,出站通道适配器只支持输出消息\
    //channel adapter属于message endpoint,后者与Message、channel同属于spring integration的三大部分
    //  ,以达到不同系统之间交互的问题,通过异步消息驱动来达到系统交互时系统之间的松耦合
	@Bean	
    public MessageProducerSupport mqttInbound(MqttPahoClientFactory mqttClientFactory) {
		
		//可以订阅多个topic
        MqttPahoMessageDrivenChannelAdapter adapter = 
        		new MqttPahoMessageDrivenChannelAdapter(serverId,
        			mqttClientFactory, new String[]{
        					CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ + "+", 
        					CommConst.TOPIC_SUBCRIBE_CARD_REG_POST_CARD + "+",
        					CommConst.TOPIC_SUBCRIBE_SCAN_LOGIN_CODE_SPEC_REQ + "+"
        			}
        		) ;
        adapter.setCompletionTimeout(20000);
        adapter.setConverter(new DefaultPahoMessageConverter());
        adapter.setQos(2);
        return adapter;
    }
	
	//IntegrationFlow来定义系统继承流程,
	//通过IntegrationFlows和IntegrationFlowBuilder来实现使用Fluent API来定义流程
	//可分流: payload -> category -> channel
	@Bean
    public IntegrationFlow mqttInFlow(MqttPahoClientFactory mqttClientFactory) {
        return IntegrationFlows.from(mqttInbound(mqttClientFactory))
                //.transform(p -> p + ", received from MQTT")
                //.handle(logger())
                .handle(mqttMessageConsumer)
                .get();
    }
	
	//暂时没用
//	private LoggingHandler logger() {
//        LoggingHandler loggingHandler = new LoggingHandler("INFO");
//        loggingHandler.setLoggerName("siSample");
//        return loggingHandler;
//    }
	
	//-----------------producer
	/**
     * The Service Activator is the endpoint type for connecting any Spring-managed Object to an input channel 
     * so that it may play the role of a service. If the service produces output, it may also be connected to 
     * an output channel. Alternatively, an output producing service may be located at the end of a processing 
     * pipeline or message flow in which case, the inbound Message's "replyChannel" header can be used. 
     * This is the default behavior if no output channel is defined, and as with most of the configuration options 
     * you'll see here, the same behavior actually applies for most of the other components we have seen.
     * 
     * refer to: 
     * https://docs.spring.io/spring-integration/docs/2.0.0.RC1/reference/html/service-activator.html
     * 
     * @return
     */
    @Bean
    @ServiceActivator(inputChannel = "mqttOutboundChannel")
    public MessageHandler mqttOutbound(MqttPahoClientFactory mqttClientFactory) {
    	//这里不用new DefaultMqttPahoClientFactory(),因spring本身已经初始化这个Factory对象,加个参数就行了
        MqttPahoMessageHandler messageHandler =  new MqttPahoMessageHandler(clientId, mqttClientFactory);
        messageHandler.setAsync(true);
        messageHandler.setDefaultTopic(defaultTopic);
        messageHandler.setDefaultQos(2);
        return messageHandler;
    }    
    
 // 暂无用途
//  @Bean
//  public MessageChannel mqttOutboundChannel() {
//      return new DirectChannel();
//  }
	

	//-----------------public
	@Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        factory.setUserName(username);
        factory.setPassword(password);
        factory.setServerURIs(new String[]{hostUrl});
        factory.setKeepAliveInterval(2);
        return factory;
    }
}

使用也是很简单:

@Component
public class MqttMessageConsumer implements MessageHandler{
	final org.slf4j.Logger log = LoggerFactory.getLogger(MqttMessageConsumer.class);
	
	@Autowired Utils utils;
	@Autowired private MqttMessageProducer mqttMessageProducer;	
	@Autowired private LibraryCardRepository libraryCardRepository;
	
	@Override
	public void handleMessage(Message<?> message) throws MessagingException {
		MessageHeaders headers = message.getHeaders();
		String topic = headers.get("mqtt_topic", String.class);
		String content = (String)message.getPayload();
		log.debug(content);
		
		// 终端请求办证二维码规范
		if (topic.startsWith(CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ)) {
			// 生成业务参考号,并将业务办理缓存
			String refID = String.valueOf(utils.idGenerator().nextId());
			String reply = JsonUtil.replaceFieldsOfJsonstr(
					CommConst.JSON_CARD_REG_CODE_SPEC, new String[][] {{"refID", refID}});			
			// 在redis中注册业务,并有时限:15分钟
			utils.redisTemplate().opsForValue().set(refID, content, CommConst.TIMEOUT_MINUTES, TimeUnit.MINUTES);
			// 发送二维码规范
			mqttMessageProducer.sendToMqtt(reply
					, CommConst.TOPIC_PUBLISH_CARD_REG_CODE_SPEC 
					// + 设备号
					+ topic.replace(CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ, ""));
		// 终端发卡结果
		} else if (topic.startsWith(CommConst.TOPIC_SUBCRIBE_CARD_REG_POST_CARD)) {
			//TODO 测试一下,可能存在json string转对象,即使String非对象的内容也能生成了一对象 
			
			// 成功即将已缓存的成功数据保存到数据库中
			if ("true".equals(JsonUtil.jsonstr2Map(content).get("result"))) { 
				// 更新发卡成功的数据到数据库
				LibraryCard entity = utils.getEntityFromCache(
						topic.replace(CommConst.TOPIC_SUBCRIBE_CARD_REG_CODE_SPEC_REQ, ""), LibraryCard.class);
				
				if (entity != null) {
					libraryCardRepository.save(entity);
				}
			}
		}
	}
}

@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttMessageProducer {
	//payload或者data是发送消息的内容
	
	//topic是消息发送的主题
	
	//qos是mqtt 对消息处理的几种机制分为0,1,2 其中0表示的是订阅者没收到消息不会再次发送,消息会丢失,1表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息,2相比多了一次去重的动作,确保订阅者收到的消息有一次
	//当然,这三种模式下的性能肯定也不一样,qos=0是最好的,2是最差的
	
	//链接到handler,handler跟这些参数怎么约定的?
	void sendToMqtt(String data);
    void sendToMqtt(String payload, @Header(MqttHeaders.TOPIC) String topic);
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}

2.8 swagger2集成

swagger可以自动生成接口文档,不用再用诸如gitbook生成文档了,也算是解决了一部分事情,引入依赖:

<!-- 整合swagger,用于生成接口 文档  http://localhost:8080/swagger-ui.html-->
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.7.0</version>
</dependency>

初始化配置一下:

//对方法的注解有要求:
//@ApiOperation("获取用户信息")
//@ApiImplicitParam(name = "name", value = "用户名", dataType = "string", paramType = "query")
//@GetMapping("/test/{name}")
//上边的方法是用 @GetMapping 注解,如果只是使用 @RequestMapping 注解,不配置 method 属性,那么 API 文档会生成 7 种请求方式。
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
	@Bean
	public Docket accessToken() {
		return new Docket(DocumentationType.SWAGGER_2)
				.groupName("api")// 定义组
			    .select() // 选择那些路径和 api 会生成 document
			    .apis(RequestHandlerSelectors.basePackage("com.xxx.qingdu.controller")) // 拦截的包路径
			    .paths(PathSelectors.regex("/*/.*"))// 拦截的接口路径
			    .build() // 创建
			    .apiInfo(apiInfo()); // 配置说明
	}

	private ApiInfo apiInfo() {
		return new ApiInfoBuilder()//
				.title("氢读后台接口")// 标题
				.description("相关接口的详细定义")// 描述
				.termsOfServiceUrl("http://qingdu.com")//
				.contact(new Contact("daniel", "http://qingdu.com", "136099332@qq.com"))// 联系
				.version("1.0")// 版本
				.build();
	}
}

2.9 spring data jpa介绍

关于jpa,已经有一篇文章介绍过了,见这里。其优点也不言而喻:

  • 不再为是新增还是更新烦恼;

  • 大量的批处理、统计、排序、分页等API;

  • 这种方法不但避免了hql语句,可读性也很强:

cartBookRepository.findByIdCartAndIsVisibleOrderByIsChosenDesc(idCart, true)

也有一些注意事项:

  • save后的对象不是在原参数对象上修改,而是返回是数据库中的数据转换而成的对象,数据库中不含的数据将丢失。
@Override
@Caching(put = {
        @CachePut(value = "ggh", key = "'book_idbook_'.concat(#book.id)"),
        @CachePut(value = "ggh", key = "'book_isbn_'.concat(#book.publishIsbn)")
})
public Book saveBook(Book book) {
    bookRepository.save(book); //注意:返回的是save到book表的内容,不包含其它信息,比如对应数据
    return book; //由于数据库的内容与原对象内容没差别,为了保留其它信息,返回原对象
}

2.10 几个小配置

#beautify json
spring.jackson.serialization.indent_output=true

#auto compile 
spring.devtools.restart.enabled=true
spring.devtools.restart.exclude=static/**,public/**,templates/**

其它

这两个小依赖背后的功能有点小意思:

<!--   监控,比如/actuator/httptrace,/actuator/beans -->
<!-- https://www.baeldung.com/spring-boot-actuators -->
<!-- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-endpoints -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!--   在结果集中新增接口链接提示 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

2.11 @JsonView的应用

@JsonView是为了在返回数据项时,屏蔽一些字段,刚好在2.4 表单form bean与entity bean分开介绍了From表单的简化, 这里是对Response的简化。

具体办法是在bean类里的字段上注解,有点分组的意思,一个多版(如果需要的话),一个就是简版了。

先定义两个空接口,实质上是一种约定名称

public interface DetailView extends SimpleView{
}
public interface SimpleView {
}

然后在bean上注解:

@JsonView(DetailView.class)
@Column(name="num_phone")
String numPhone;

@JsonView(SimpleView.class)
@Column(name="id_union")
long idUnion;

最后在controller上引用约定:

@GetMapping("/wechat/{openId}")
@JsonView(SimpleView.class)
public UserByWechat getWechatUserByOpenId(@PathVariable String openId) {
    return userWechatService.getWechatUserByOpenId(openId);
}

这样,返回结果时就不会包含num_phone了,另外没有注解JsonView的字段不会返回:

{
  "id": 509028065599152100,
  "createTime": "2018-11-05T07:35:48.000+0000",
  "lastTime": "2018-11-05T07:37:12.000+0000",
  "idUnion": 509028065477517300,
  "nameWechat": "string",
  "idUnionWechat": "string",
  "idOpen": "st"
}

另外,经过实践,其实2.4的做法通过JsonView可以解决,去掉form bean,只有entity bean:

  1. 返回减元属性以述已经解决
  2. 防止Formbean的属性把entity bean的覆盖?用BeanUtils.copyProperties(userMini, exists, new String[] {"id", "idUnion", "createTime", "isDelete"}), 第三个参数就是解决哪些字段不要拷贝。

@JsonIgnore也有类似功效 https://www.devglan.com/spring-security/spring-boot-jwt-auth

2.12 @JsonProperty的应用

这个是解决返回的json结果的字段名,字段名可以自定义的。特别是在用通用字段时,比如都继承基类字段ID,可读性差,给它重新 起个名字还是有必要的:

@Override
@JsonProperty("idWechartUser")
public long getId() {
    return super.getId();
}

这样,返回的结果就变了,而不是上段所示的id

{
  "createTime" : "2018-11-05T07:35:48.000+0000",
  "lastTime" : "2018-11-05T07:37:12.000+0000",
  "idUnion" : 509028065477517312,
  "nameWechat" : "string",
  "idUnionWechat" : "string",
  "idOpen" : "st",
  "idWechartUser" : 509028065599152128
}

2.13 统一加返回信息的code/error message

简而言之的需求就是:有的是业务逻辑的异常情况,没有throw exception,而是用code/message来封装一个返回数据 ,在之前的项目中是用一个统一的exception handler,业务逻辑的不正常直接throw自定义的exception,然后根据自定义对象 (封装有code/message在java中定义死了/http status)再给Response设定http status以及body。但是,这仅仅处理异常,正常的数据返回走的另一通道。

参考自这里, 解决思路是返回一个map,如果里面包含两项基本内容,code/以及Data(如果没异常)或者errmsg。这样不管是正常或异常的义务处理,都走一条通常。

具体做法如下:

  • 定义一个base controller,用途很简单,定义一个return封装,以及一个properties引用
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

//https://github.com/nosqlcoco/springboot-weapp-demo/blob/master/src/main/java/com/weapp/controller/BaseController.java#L21:31
@Component
public abstract class BaseController {
    @Autowired
    private ImmutableMap<String, String> errorCodeMap;

    /**
     * 接口数据返回
     * @param errorCode
     * @param data
     * @return
     */
    protected Map<String,Object> rtnParam(Integer errorCode, Object data) {
        //正常的业务逻辑
        if(errorCode == 0){
            return ImmutableMap.of("errcode", errorCode, "data", (data == null)? new Object() : data);
        }else{
            return ImmutableMap.of("errcode", errorCode, "errmsg", errorCodeMap.get(String.valueOf(errorCode)));
        }
    }
}

ImmutableMap是google的一个工具map,errorCodeMap在程序启动时即初始化:

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.IOException;
import java.util.Properties;

@EnableCaching //开启缓存功能
@SpringBootApplication //main方式启动
//@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
public class Starter extends org.springframework.boot.web.servlet.support.SpringBootServletInitializer{

    private static ImmutableMap<String, String> errorCodeMap = null;
        
    static {
        try {
            Properties prop = PropertiesLoaderUtils.loadAllProperties("error_code.properties");
            errorCodeMap = Maps.fromProperties(prop);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //覆盖单例,在BaseController引用此对象
    @Bean
    public ImmutableMap<String, String> errorCodeMap(){
        return errorCodeMap;
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Starter.class);
    }

	public static void main(String[] args) {
        SpringApplication.run(Starter.class, args);  
    }
}

应用也很简单:

//返回一个map
return rtnParam(0, JsonUtil.jsonstr2Map("{\"sessionId\": \"" + sId +  "\"}"));

code为0时放data,通常放的是map/bean,会被自动转为json格式。由于controller接口返回的一个map,所以不调rtnParam也是可以的,比如请求第三方接口,其返回是的json,然后我可以直接返回。 当然这时不一定是符合自己定义的数据规范。

2.14 @Cacheable的应用

基本的应用就是在方法上加注解,然后框架自动操作对象与缓存。下面介绍使用redis作为缓存,首先引入基于redis的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis的键生成策略和redis操作template:

import java.lang.reflect.Method;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
 * redis的缓存配置
 * 更多配置可以参考:https://www.cnblogs.com/yueshutong/p/9381540.html
 * 
 * @author tao
 * 
 * TODO 这个类的配置问题
 *
 */
@Configuration
@EnableCaching
//https://blog.csdn.net/micro_hz/article/details/76599632
public class RedisConfig extends CachingConfigurerSupport {
	
	//缓存管理器,扩展
	@Bean 
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheManager cacheManager = RedisCacheManager.create(factory);
        //设置缓存过期时间 
        //cacheManager.setDefaultExpiration(10000);
        return cacheManager;
    }
		
	/*//缓存管理器,扩展
	@Bean 
    public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        //设置缓存过期时间 
        cacheManager.setDefaultExpiration(10000);
        return cacheManager;
    }*/
	
	@Bean
	@Override
	public KeyGenerator keyGenerator() {
		//未解决的问题:缓存中的内容如何同步更新?
		return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                //sb.append(target.getClass().getName());
                //sb.append(".").append(method.getName()).append("_"); //不能用冒号?
                
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
	}
	
	@Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
        //设置序列化工具        
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        
        @SuppressWarnings({ "rawtypes", "unchecked" })
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        StringRedisTemplate template = new StringRedisTemplate(factory);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

在repository中引用的话,方法如下:

import com.xxx.qingdu.bean.App;

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.io.Serializable;

@Repository
@CacheConfig(cacheNames = "app") 
public interface AppRepository extends CrudRepository<App, Serializable>, JpaSpecificationExecutor<App> {
	//@Cacheable(value="app", keyGenerator="keyGenerator") value指定命名空间,也可以在类上用@CacheConfig(cacheNames = "app") 
	//坑1:Null key returned for cache operation (maybe you are using named params on classes without debug info?)
		//解决办法:不能在repository层定义,或者不设属性key,而是keyGenerator https://www.cnblogs.com/sxdcgaq8080/p/8119081.html
	//坑2:java.lang.ClassCastException: com.xxx.qingdu.bean.App cannot be cast to com.xxx.qingdu.bean.App
		//解决办法:注释掉热加载的依赖<artifactId>spring-boot-devtools</artifactId>,原因?https://blog.csdn.net/qq_33101675/article/details/78231053
	
	//由于是接口,如何验证cache已生效?在数据表中修改数据,如果读的数据库数据,校验通不过
	//更多用法:https://www.cnblogs.com/yueshutong/p/9381540.html https://blog.csdn.net/weixin_41888813/article/details/82754659

	@Cacheable(keyGenerator="keyGenerator")
	public App findByAppId(String appId);
}

App对象要实现序列化接口,最后,运行时可见redis已经保存了一个appId为键的对象,若要更新这个缓存,在service实现类中使用如下方法:

@CachePut(value="app", key="#app.appId")
public App updateApp(App app) {
    return appRespository.save(app);
}

更多注解参考这里

其它实操:

  • 取参、拼接key、加条件、以及多项缓存操作定义在一个方法里
@Caching(put = {
            @CachePut(value = "ggh", key = "'libraries_idbook_'.concat(#idBook)", condition = "#librariesContainsBook.size() > 0")
        },
        evict = {
            @CacheEvict(value = "ggh", key = "'libraries_idbook_'.concat(#idBook)", condition = "#librariesContainsBook.size() == 0")
        }
)
public List<LibraryBook> updateLibraryBookAvailable(long idBook, List<LibraryBook> librariesContainsBook) 
  • cache注解的方法在同一个类中不能相互调用,否则,并不利用缓存。所以如果数据有一些关联,而存在互调,最好根据不同实体分散在多个service中。